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

  1. Run gateway and daemon from matching builds so AdminResponseBody variants agree
  2. Check daemon logs for why the reply body didn't match the InviteIssue request
  3. 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

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


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/8dab2c8e7c3b2b8a. Report an issue: GitHub.