astrid-runtime/astrid · error · GatewayError::Internal
unexpected response shape: {other:?}
Error message
unexpected response shape: {other:?} What it means
After a successful daemon round-trip, issue_invite expects an AdminResponseBody::Invite; a daemon-reported error becomes Forbidden, but any other body variant is an unrecognized protocol shape and is raised as an Internal error. This catches version skew or daemon bugs where the reply doesn't match the request kind.
Source
Thrown at crates/astrid-gateway/src/routes/invites.rs:97
let bytes = axum::body::to_bytes(req.into_body(), 64 * 1024)
.await
.map_err(|e| GatewayError::BadRequest(format!("body read: {e}")))?;
let body: IssueRequest = serde_json::from_slice(&bytes)?;
let client = state.admin_client_for(&caller)?;
let resp = client
.request(AdminRequestKind::InviteIssue {
group: body.group,
expires_secs: body.expires_secs,
max_uses: body.max_uses,
metadata: body.metadata,
})
.await
.map_err(|e| GatewayError::Internal(anyhow::anyhow!("daemon request: {e}")))?;
match resp {
AdminResponseBody::Invite(invite) => Ok(Json(IssueResponse { invite })),
AdminResponseBody::Error(msg) => Err(GatewayError::Forbidden { reason: msg }),
other => Err(GatewayError::Internal(anyhow::anyhow!(
"unexpected response shape: {other:?}"
))),
}
}
/// `OpenAPI` schema mirror of [`astrid_core::kernel_api::InviteSummary`].
/// Never constructed; resolves the `value_type` on
/// [`ListResponse::invites`] to a typed schema. Keep it
/// field-for-field with the serialized shape of `InviteSummary` —
/// note the field is `token_fingerprint` (not `fingerprint`), and
/// `issued_at_epoch` is always present.
#[derive(ToSchema)]
pub struct InviteSummaryView {
/// Domain-separated `blake3:<hex>` fingerprint of the token. Raw tokens are never
/// leaked through list responses.
pub token_fingerprint: String,
/// Group the redeemer will join.
pub group: String,View on GitHub (pinned to affd8760f4)
Solutions
- Run gateway and daemon from matching builds so AdminResponseBody variants agree
- Check daemon logs for why the reply body didn't match the InviteIssue request
- Extend the match to handle the unexpected variant explicitly if it's a legitimate new response
Example fix
// before
other => Err(GatewayError::Internal(anyhow::anyhow!(
"unexpected response shape: {other:?}"
))),
// after
AdminResponseBody::Invite(invite) => Ok(Json(IssueResponse { invite })),
AdminResponseBody::Error(msg) => Err(GatewayError::Forbidden { reason: msg }),
other => {
tracing::error!(response = ?other, "unexpected AdminResponseBody for InviteIssue");
Err(GatewayError::Internal(anyhow::anyhow!("unexpected response shape: {other:?}")))
} Defensive patterns
Strategy: try-catch
Validate before calling
// Confirm protocol compatibility before sending invite requests
if client.protocol_version()? != GATEWAY_PROTOCOL_VERSION {
return Err(anyhow::anyhow!("gateway/daemon version skew"));
} Type guard
fn is_invite_response(resp: &AdminResponseBody) -> bool {
matches!(resp, AdminResponseBody::Invite(_) | AdminResponseBody::Error(_))
} Try / catch
match issue_invite(State(state), caller, Json(body)).await {
Ok(resp) => resp,
Err(GatewayError::Internal(e)) if e.to_string().contains("unexpected response shape") => {
tracing::error!(%e, "daemon returned wrong body for InviteIssue");
upgrade_or_reconnect()
}
Err(e) => handle(e),
} Prevention
- Deploy gateway and daemon together or gate on a protocol version handshake
- Add exhaustively-tested mappings for every AdminResponseBody variant per request kind
- Include unit tests where the daemon returns each variant for InviteIssue
When it happens
Trigger: The daemon responds to InviteIssue with a body other than Invite or Error — e.g. InviteList, a new enum variant added in a newer daemon, or a corrupted/deserialized-wrong payload.
Common situations: Gateway and daemon at different versions; a daemon that mis-routes the reply for the request kind; test doubles returning the wrong AdminResponseBody variant.
Related errors
- unexpected response shape: {other:?}
- unexpected response from kernel: {other:?}
- unexpected response shape for GetCapsuleMetadata: {other:?}
- daemon request: {e}
- MCP gateway rejected {operation:?}: {}
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/8dab2c8e7c3b2b8a.
Report an issue: GitHub.