astrid-runtime/astrid · error · GatewayError::Internal
daemon request: {e}
Error message
daemon request: {e} What it means
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.
Source
Thrown at crates/astrid-gateway/src/routes/principals.rs:603
req.extensions()
.get::<CallerContext>()
.ok_or(GatewayError::Unauthorized)
}
// Both helpers consume their argument logically (wrap and discard);
// clippy::needless_pass_by_value fires because they only `Display` /
// `Debug` the value. Taking by value keeps `map_err(daemon_internal)`
// usable as a one-line closure replacement throughout the routes —
// the by-reference shape would force every call site to write
// `map_err(|e| daemon_internal(&e))`.
// The admin-client request path still surfaces `anyhow::Error` (it is not part
// of this change's typed-error migration — a follow-up); every failure here maps
// to 500. The bus-direct kernel-request path uses the typed
// [`daemon_kernel_error`](crate::routes::daemon_kernel_error) instead, which can
// distinguish a 504 timeout.
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn daemon_internal(e: anyhow::Error) -> GatewayError {
GatewayError::Internal(anyhow::anyhow!("daemon request: {e}"))
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn unexpected(other: AdminResponseBody) -> GatewayError {
GatewayError::Internal(anyhow::anyhow!(
"unexpected admin response shape: {other:?}"
))
}
/// Read the request body as JSON, capping at 64 `KiB` to bound any
/// pathological inbound on the otherwise-unauthenticated edge.
pub(crate) async fn read_json_body<T: serde::de::DeserializeOwned>(
req: Request<axum::body::Body>,
) -> GatewayResult<T> {
let bytes = axum::body::to_bytes(req.into_body(), 64 * 1024)
.await
.map_err(|e| GatewayError::BadRequest(format!("body read: {e}")))?;
Ok(serde_json::from_slice(&bytes)?)View on GitHub (pinned to affd8760f4)
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.
Example fix
// before resp.map_err(daemon_internal)? // after resp.await.map_err(daemon_kernel_error)? // typed mapping: timeouts become 504, kernel errors become 4xx
Defensive patterns
Strategy: fallback
Validate before calling
// health-check the daemon before issuing the request
let healthy = reqwest::get("http://gateway/healthz").await?.status().is_success();
if !healthy { return Err("daemon backend unavailable".into()); } Try / catch
match gateway.call(admin_req).await {
Ok(resp) => resp,
Err(e) if e.to_string().starts_with("daemon request:") => {
// retry once, then fail over / alert
gateway.call(admin_req).await.map_err(report_and_fallback)?
}
Err(e) => return Err(e),
} Prevention
- 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.
When it happens
Trigger: 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)`.
Common situations: 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.
Related errors
- unexpected response shape for GetCapsuleMetadata: {other:?}
- unexpected admin response shape: {other:?}
- daemon request: {e}
- daemon kernel-request: {e}
- gateway has no live capsule provider probe
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/31f31e1d01dd4f65.
Report an issue: GitHub.