Skip to content
Documentation GitHub
Integration Issues

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_import and 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

  1. Checked the JSON body format — fields were correct and camelCase as expected
  2. Compared Tauri command handler signatures with bridge route handlers — parameter types matched
  3. 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:

  1. The Tauri command takes a single complex struct parameter (e.g., request: ImportRequest)
  2. The frontend wraps it as { request: { ... } } (Tauri’s named-parameter convention)
  3. 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 curl before 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.rsImportRequestWrapper implementation
  • apps/desktop/src-tauri/src/commands/import.rs — Tauri command signature (compare parameter names)
  • Tauri v2 docs: Commands — named parameter convention

Was this page helpful?