{"record":{"id":"d47c036ef285fbdd","repo":"gitbutlerapp/gitbutler","slug":"handler-task-panicked-e","errorCode":null,"errorMessage":"handler task panicked: {e}","messagePattern":"handler task panicked: (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/but-server/src/lib.rs","lineNumber":70,"sourceCode":"\n#[derive(Clone)]\nstruct AppState {\n    broadcaster: Arc<Mutex<Broadcaster>>,\n    extra: Extra,\n    app_settings: AppSettingsWithDiskSync,\n}\n\n/// Converts a synchronous command handler into an axum `MethodRouter` that works with\n/// `Router::route`.\nfn but_post<F, S>(f: F) -> MethodRouter<S, Infallible>\nwhere\n    F: Fn(serde_json::Value) -> anyhow::Result<serde_json::Value> + Copy + Send + Sync + 'static,\n    S: Clone + Send + Sync + 'static,\n{\n    post(move |Json(params)| async move {\n        let res = tokio::task::spawn_blocking(move || f(params))\n            .await\n            .unwrap_or_else(|e| Err(anyhow::anyhow!(\"handler task panicked: {e}\")));\n        cmd_result_to_json(res)\n    })\n}\n\n/// Converts an asynchronous command handler into an axum `MethodRouter` that works with\n/// `Router::route`.\nfn but_post_async<F, Fut, S>(f: F) -> MethodRouter<S, Infallible>\nwhere\n    F: Fn(serde_json::Value) -> Fut + Copy + Send + Sync + 'static,\n    Fut: Future<Output = anyhow::Result<serde_json::Value>> + Send,\n    S: Clone + Send + Sync + 'static,\n{\n    post(move |Json(params)| async move {\n        let res = f(params).await;\n        cmd_result_to_json(res)\n    })\n}\n","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-server/src/lib.rs#L52-L88","documentation":"`but-server` runs every synchronous command handler inside `tokio::task::spawn_blocking`; if the handler panics, `JoinError` is captured and converted into this anyhow error, which becomes the JSON-RPC/HTTP error response instead of crashing the axum process. The panic message and backtrace are embedded in `{e}`. So this error always means: a command handler hit a Rust panic (unwrap on None, index out of bounds, poisoned lock, assertion).","triggerScenarios":"A handler calling `.unwrap()` on an Option/Result that is None/Err for particular inputs (missing project, empty repo, unborn branch); a lock poisoned by an earlier panic; slicing/indexing bugs; `expect` on invariant violations — all invoked through an axum `but_post` route.","commonSituations":"Server-side command works in the happy path but panics on edge-case payloads; first panic poisons a Mutex so subsequent requests on the same state also panic; regressions introduced by refactors leaving a None case unhandled.","solutions":["Read the panic message inside the returned error text — it names the exact unwrap/expect and line that panicked","Reproduce the handler directly (unit test with the same JSON params) to get a full backtrace","Replace the panicking unwrap/expect with `ok_or_else(anyhow!)`/`?` so the failure becomes a normal error","If the panic came from a poisoned lock, find and fix the first panic; consider not sharing state across Mutex without recovery"],"exampleFix":"// before (handler registered via but_post)\nfn get_project(params: serde_json::Value) -> anyhow::Result<serde_json::Value> {\n    let id = params[\"id\"].as_str().unwrap(); // panics on missing/null id -> 'handler task panicked'\n    ...\n}\n\n// after\nfn get_project(params: serde_json::Value) -> anyhow::Result<serde_json::Value> {\n    let id = params\n        .get(\"id\")\n        .and_then(|v| v.as_str())\n        .ok_or_else(|| anyhow::anyhow!(\"missing string field 'id'\"))?;\n    ...\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Server side: the spawn_blocking wrapper already catches JoinError; keep it that way and\n// add structured logging so panics are never silent:\nlet res = tokio::task::spawn_blocking(move || f(params))\n    .await\n    .unwrap_or_else(|e| {\n        tracing::error!(\"handler panicked: {e}\");\n        Err(anyhow::anyhow!(\"handler task panicked: {e}\"))\n    });\n// Client side: treat this error as a server bug — report the request payload; do not retry.","preventionTips":["Forbid unwrap/expect in command handlers; review diffs for them at PR time (clippy lints like unwrap_used help)","Return Result from handlers and validate all JSON params explicitly","Add unit tests per handler for missing/null fields and empty-state inputs","Watch for lock poisoning: one panic can convert later requests into the same error"],"tags":["rust","server","panic","tokio","error-handling"],"backgroundTag":"task-panicked","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}