Handle spawn_blocking panics in API handlers without crashing server
Replace .expect() on JoinHandle results in verify_run_token, get_bootstrap,
and get_secret with .unwrap_or_else() that logs the panic and returns a 500.
Add ApiError::Internal variant to carry this case through the existing
IntoResponse impl.
change
commit 75564c713eecdc23f116a274c5e2f5a12ea0434b
author Claude <noreply@anthropic.com>
date
parent a32c26dd
diff --git a/quire-server/src/quire/web/api.rs b/quire-server/src/quire/web/api.rs
index c9dddf5..f3fd11b 100644
--- a/quire-server/src/quire/web/api.rs
+++ b/quire-server/src/quire/web/api.rs
@@ -47,6 +47,8 @@ enum ApiError {
     Unauthorized,
     #[error("gone")]
     Gone,
+    #[error("internal error")]
+    Internal,
     #[error(transparent)]
     Db(rusqlite::Error),
     #[error(transparent)]
@@ -124,7 +126,10 @@ async fn verify_run_token(
         }
     })
     .await
-    .expect("blocking task panicked")?;
+    .unwrap_or_else(|e| {
+        tracing::error!(error = ?e, "spawn_blocking task panicked in verify_run_token");
+        Err(ApiError::Internal)
+    })?;
 
     let mut req = axum::extract::Request::from_parts(parts, body);
     req.extensions_mut().insert(run_id);
@@ -196,7 +201,10 @@ async fn get_bootstrap(
             })
         })
         .await
-        .expect("blocking task panicked")?;
+        .unwrap_or_else(|e| {
+            tracing::error!(error = ?e, "spawn_blocking task panicked in get_bootstrap");
+            Err(ApiError::Internal)
+        })?;
 
     Ok(axum::Json(bootstrap))
 }
@@ -225,7 +233,10 @@ async fn get_secret(
             .to_string())
     })
     .await
-    .expect("blocking task panicked")?;
+    .unwrap_or_else(|e| {
+        tracing::error!(error = ?e, "spawn_blocking task panicked in get_secret");
+        Err(ApiError::Internal)
+    })?;
 
     Ok(axum::Json(serde_json::json!({ "value": value })))
 }