Tauri IPC Named-Parameter Wrapping in HTTP Bridge Handlers
Tauri IPC Named-Parameter Wrapping in HTTP Bridge Handlers
Problem
HTTP bridge routes return 422 errors with serde deserialization failures, even though the frontend sends correctly structured JSON. The exact same request works in the real Tauri app.
Symptoms:
execute_importand similar commands return HTTP 422- Serde errors like
missing field "source_path"despite the field being present - Newly added bridge routes fail while existing routes work
Investigation
Steps Tried
- Checked the JSON body format — fields were correct and camelCase as expected
- Compared Tauri command handler signatures with bridge route handlers — parameter types matched
- Logged the raw request body on the bridge side — discovered the body was wrapped in an extra layer
Root Cause
Tauri’s invoke() wraps command arguments in an object keyed by the Rust parameter name. When the frontend calls:
invoke("execute_import", { request: importData })Tauri’s IPC layer receives { "request": { "sourcePath": "...", ... } } and automatically unwraps the request field
before passing it to the Rust handler:
// Tauri auto-unwraps: receives ImportRequest directly#[tauri::command]fn execute_import(request: ImportRequest) -> Result<...> { ... }The HTTP bridge doesn’t have Tauri’s auto-unwrap. When the same frontend code calls the bridge, the Axum handler receives the full wrapper:
{ "request": { "sourcePath": "/path", "importMode": { "type": "inPlace" } } }But the handler tries to deserialize directly into ImportRequest, which fails because the outer { "request": ... }
wrapper doesn’t match the ImportRequest fields.
Solution
Create an explicit wrapper struct matching Tauri’s naming convention:
/// Wrapper struct matching Tauri's invoke convention./// When the frontend calls `invoke("execute_import", { request })`,/// the JSON body is `{ "request": { ... } }`. Tauri auto-unwraps this,/// but the HTTP bridge receives the full wrapper.#[derive(Deserialize)]pub struct ImportRequestWrapper { pub request: ImportRequest,}
pub async fn execute_import( Extension(state): Extension<Arc<BridgeState>>, Json(wrapper): Json<ImportRequestWrapper>,) -> Result<Json<serde_json::Value>, CommandError> { let request = wrapper.request; // ... business logic using unwrapped request}When Wrappers Are NOT Needed
Most bridge routes use simple flat structs:
// Frontend: invoke("create_page", { title, parentSlug })// JSON body: { "title": "...", "parentSlug": "..." }
#[derive(Deserialize)]#[serde(rename_all = "camelCase")]pub struct CreatePageArgs { pub title: String, pub parent_slug: Option<String>,}This works because Tauri unwraps each named parameter individually, and the bridge struct mirrors the unwrapped shape. The wrapper is only needed when:
- The Tauri command takes a single complex struct parameter (e.g.,
request: ImportRequest) - The frontend wraps it as
{ request: { ... } }(Tauri’s named-parameter convention) - The bridge handler would otherwise try to deserialize the inner struct from the outer wrapper
Implementation Notes
- Always add
#[serde(rename_all = "camelCase")]to the inner struct (matching Tauri’s automatic camelCase translation) - Add a comment explaining the Tauri convention on every wrapper struct
- Test new routes with
curlbefore browser testing:curl -X POST localhost:9990/invoke/cmd -H 'Content-Type: application/json' -d '{"paramName": {...}}'
Prevention
Best Practices
- When adding a new bridge route, check the Tauri command signature — if it takes a named struct parameter, create a wrapper
- Use
tracing::debug!to log the raw JSON body during development: helps catch wrapping mismatches early - Document the Tauri convention in the bridge’s module-level rustdoc
Warning Signs
- Route works in Tauri but returns 422 on the bridge
- Serde error mentions a field that IS present in the request (it’s looking at the wrong nesting level)
- Route works for simple
{ key: value }payloads but fails for complex nested structs
References
apps/http-bridge/src/routes/import.rs—ImportRequestWrapperimplementationapps/desktop/src-tauri/src/commands/import.rs— Tauri command signature (compare parameter names)- Tauri v2 docs: Commands — named parameter convention
Tauri 2 Structured Error Serialization Produces [object Object] Next
React Hooks exhaustive-deps Fix Patterns
Was this page helpful?
Thanks for your feedback!