astrid-runtime/astrid · error · GatewayError::Internal
unexpected admin response shape: {other:?}
Error message
unexpected admin response shape: {other:?} What it means
The `unexpected` helper builds a 500 `GatewayError::Internal` when the daemon returns an `AdminResponseBody` that does not match the variant the caller expected. It is invoked from handlers like `grant_caps`, `revoke_caps`, `write_env_inner`, `list_groups`, `create_group`, and `modify_group` in the catch-all arm of their response matches. It indicates the gateway and daemon disagree on the admin response protocol.
Source
Thrown at crates/astrid-gateway/src/routes/principals.rs:608
// 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
- Read the `{other:?}` debug payload in the 500 response/logs to see which variant actually arrived.
- Align gateway and daemon to the same version (redeploy both together).
- Add a match arm for the observed variant in the calling handler, mapping `AdminResponseBody::Error` to a client-appropriate GatewayError instead of 500.
- Check for a daemon-side error log corresponding to the failed admin request.
Example fix
// before
match resp {
AdminResponseBody::Caps(c) => Ok(Json(c)),
other => Err(unexpected(other)),
}
// after
match resp {
AdminResponseBody::Caps(c) => Ok(Json(c)),
AdminResponseBody::Error(msg) => Err(GatewayError::Forbidden { reason: msg }),
other => Err(unexpected(other)),
} Defensive patterns
Strategy: type-guard
Type guard
fn as_caps(resp: &AdminResponseBody) -> Option<&Caps> {
match resp { AdminResponseBody::Caps(c) => Some(c), _ => None }
} Try / catch
try {
...
} catch (e) {
if (e.status === 500 && /unexpected admin response shape/.test(e.message)) {
// protocol mismatch: refresh both gateway+daemon, log full body
} else { throw e; }
} Prevention
- Deploy gateway and daemon together so `AdminResponseBody` variants stay in sync.
- Handle `AdminResponseBody::Error` explicitly in every admin handler match.
- Add exhaustive-match lint/CI checks on response enums.
- Log the full debug payload of any catch-all arm before returning 500.
When it happens
Trigger: A capability/group/env admin handler matches on the daemon's `AdminResponseBody` and falls through to `Self::unexpected(other)` because the daemon returned a different variant (e.g. `Error`, or a newer/older protocol variant) instead of the expected success payload.
Common situations: Gateway and daemon built from different versions after a partial deploy; the daemon rejected the request and returned an error variant the handler didn't expect; a newly added `AdminResponseBody` variant not yet handled in the gateway's match.
Understand the failure class
Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.
Related errors
- unexpected response shape for GetCapsuleMetadata: {other:?}
- daemon request: {e}
- daemon request: {e}
- unexpected response shape: {other:?}
- daemon kernel-request: {e}
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/323da549e362a74a.
Report an issue: GitHub.