{"record":{"id":"31f31e1d01dd4f65","repo":"astrid-runtime/astrid","slug":"daemon-request-e-31f31e","errorCode":null,"errorMessage":"daemon request: {e}","messagePattern":"daemon request: (.+?)","errorType":"http","errorClass":"GatewayError::Internal","httpStatus":500,"severity":"error","filePath":"crates/astrid-gateway/src/routes/principals.rs","lineNumber":603,"sourceCode":"    req.extensions()\n        .get::<CallerContext>()\n        .ok_or(GatewayError::Unauthorized)\n}\n\n// Both helpers consume their argument logically (wrap and discard);\n// clippy::needless_pass_by_value fires because they only `Display` /\n// `Debug` the value. Taking by value keeps `map_err(daemon_internal)`\n// usable as a one-line closure replacement throughout the routes —\n// the by-reference shape would force every call site to write\n// `map_err(|e| daemon_internal(&e))`.\n// The admin-client request path still surfaces `anyhow::Error` (it is not part\n// of this change's typed-error migration — a follow-up); every failure here maps\n// to 500. The bus-direct kernel-request path uses the typed\n// [`daemon_kernel_error`](crate::routes::daemon_kernel_error) instead, which can\n// distinguish a 504 timeout.\n#[allow(clippy::needless_pass_by_value)]\npub(crate) fn daemon_internal(e: anyhow::Error) -> GatewayError {\n    GatewayError::Internal(anyhow::anyhow!(\"daemon request: {e}\"))\n}\n\n#[allow(clippy::needless_pass_by_value)]\npub(crate) fn unexpected(other: AdminResponseBody) -> GatewayError {\n    GatewayError::Internal(anyhow::anyhow!(\n        \"unexpected admin response shape: {other:?}\"\n    ))\n}\n\n/// Read the request body as JSON, capping at 64 `KiB` to bound any\n/// pathological inbound on the otherwise-unauthenticated edge.\npub(crate) async fn read_json_body<T: serde::de::DeserializeOwned>(\n    req: Request<axum::body::Body>,\n) -> GatewayResult<T> {\n    let bytes = axum::body::to_bytes(req.into_body(), 64 * 1024)\n        .await\n        .map_err(|e| GatewayError::BadRequest(format!(\"body read: {e}\")))?;\n    Ok(serde_json::from_slice(&bytes)?)","sourceCodeStart":585,"sourceCodeEnd":621,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-gateway/src/routes/principals.rs#L585-L621","documentation":"The gateway's `daemon_internal` helper wraps any `anyhow::Error` raised while proxying a request to the astrid daemon into a 500 `GatewayError::Internal` with the message \"daemon request: {e}\". It is the non-typed catch-all for daemon round-trips; unlike the bus-direct kernel path (`daemon_kernel_error`), it cannot distinguish a timeout (504) and deliberately flattens every daemon-side failure into 500. This migration-to-typed-errors gap is acknowledged in the source comments as pending follow-up work.","triggerScenarios":"Any admin/principal route handler that awaits a daemon request and receives an Err (transport failure, daemon unavailable, daemon returned an error response, deserialization failure) converts it via `daemon_internal(e)`.","commonSituations":"The astrid daemon process is down or restarting while the gateway stays up; wrong daemon socket/endpoint configuration; daemon overloaded so the request times out (surfaced as 500 instead of 504 due to the untyped mapping); version mismatch so the daemon's response fails to deserialize.","solutions":["Check the daemon is running and reachable (process status, socket/endpoint config) and restart it if needed.","Inspect the inner `{e}` text in the 500 body/logs — it carries the underlying transport/decode error; fix that root cause.","If the failure is a timeout, migrate this call path to `daemon_kernel_error` so clients get a proper 504 instead of 500.","Verify gateway and daemon versions match so request/response shapes deserialize correctly."],"exampleFix":"// before\nresp.map_err(daemon_internal)?\n// after\nresp.await.map_err(daemon_kernel_error)? // typed mapping: timeouts become 504, kernel errors become 4xx","handlingStrategy":"fallback","validationCode":"// health-check the daemon before issuing the request\nlet healthy = reqwest::get(\"http://gateway/healthz\").await?.status().is_success();\nif !healthy { return Err(\"daemon backend unavailable\".into()); }","typeGuard":null,"tryCatchPattern":"match gateway.call(admin_req).await {\n    Ok(resp) => resp,\n    Err(e) if e.to_string().starts_with(\"daemon request:\") => {\n        // retry once, then fail over / alert\n        gateway.call(admin_req).await.map_err(report_and_fallback)?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Monitor daemon liveness and alert before clients hit the gateway.","Pin gateway and daemon to matching versions in deployment.","Prefer routes using the typed `daemon_kernel_error` path so timeouts surface as 504.","Always inspect the inner error text of 500s from daemon-proxied routes."],"tags":["gateway","daemon","internal-error","upstream-failure"],"backgroundTag":"upstream-api-error","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}