astrid-runtime/astrid · error · GatewayError::Internal

registry set_active_model reply carried neither an 'error' n

Error message

registry set_active_model reply carried neither an 'error' nor a non-null 'active_model' (an explicit-null 'active_model' is treated as absent)

What it means

`set_active_model_inner` classifies the registry's reply to PUT /api/models/active via `classify_set_active_reply`. A valid reply must be either `{ error }` (rejection -> 400) or `{ active_model: <non-null entry> }` (success -> 200). `SetActiveOutcome::Malformed` means the reply had neither an 'error' nor a non-null 'active_model' — an explicit null active_model is deliberately treated as malformed, not as success — so the gateway raises a 500 Internal.

Source

Thrown at crates/astrid-gateway/src/routes/models.rs:533

        SET_ACTIVE_RESPONSE,
        // The registry reads `model_id` (also accepted under `data`); the
        // gateway forwards the raw `id` untouched and never interprets it.
        // `corr_id` is echoed verbatim by the registry for reply correlation.
        json!({ "model_id": body.id, "corr_id": corr_id }),
        Some(corr_id.as_str()),
    )
    .await?;

    // Success: `{ "status": "ok", "active_model": <entry> }` → return the
    // persisted entry so the caller sees the canonical id the registry
    // bound. Error: `{ "error": "<msg>" }` → 400 with the registry's
    // message surfaced verbatim (the gateway does not reinterpret
    // resolution / ambiguity errors). An explicit `null` active_model is a
    // malformed reply, not a success — see `classify_set_active_reply`.
    match classify_set_active_reply(&reply) {
        SetActiveOutcome::Bound(active) => Ok(Json(active)),
        SetActiveOutcome::Rejected(message) => Err(GatewayError::BadRequest(message)),
        SetActiveOutcome::Malformed => Err(GatewayError::Internal(anyhow::anyhow!(
            "registry set_active_model reply carried neither an 'error' nor a \
             non-null 'active_model' (an explicit-null 'active_model' is treated \
             as absent)"
        ))),
    }
}

#[cfg(test)]
mod tests {
    use super::{
        GET_ACTIVE_REQUEST, GET_ACTIVE_RESPONSE, SetActiveOutcome, classify_set_active_reply,
        registry_reply_payload_json, registry_round_trip, reply_satisfies_corr_id,
    };
    use std::sync::Arc;
    use std::sync::atomic::{AtomicBool, Ordering};

    use crate::error::GatewayError;
    use crate::routes::WorkspaceContext;

View on GitHub (pinned to affd8760f4)

Solutions

  1. Check gateway and registry capsule version compatibility and upgrade/downgrade so both use the same set_active reply schema.
  2. Log the raw reply payload at the registry and gateway boundary and fix the registry so a successful bind always returns a non-null 'active_model' entry and a failure returns 'error'.
  3. Inspect `classify_set_active_reply` to confirm which reply shape was received, then patch the registry producer accordingly.
  4. Add a regression test in the registry covering the explicit-null active_model case (mirroring the gateway's classify tests).

Example fix

// before (registry reply for success)
json!({ "status": "ok", "active_model": null })
// after
json!({ "status": "ok", "active_model": bound_entry })
Defensive patterns

Strategy: validation

Validate before calling

// validate the reply shape before treating it as a bind result
let ok = reply.get("active_model").map_or(false, |v| !v.is_null())
    || reply.get("error").map_or(false, |v| v.is_string());
if !ok {
    return Err(anyhow!("malformed set_active_model reply: {reply}"));
}

Type guard

fn is_well_formed_set_active_reply(reply: &serde_json::Value) -> bool {
    reply.get("error").map_or(false, |v| v.is_string())
        || reply.get("active_model").map_or(false, |v| !v.is_null())
}

Try / catch

match classify_set_active_reply(&reply) {
    SetActiveOutcome::Bound(active) => Ok(Json(active)),
    SetActiveOutcome::Rejected(msg) => Err(GatewayError::BadRequest(msg)),
    SetActiveOutcome::Malformed => Err(GatewayError::Internal(anyhow!(
        "malformed set_active_model reply: {reply}"
    ))),
}

Prevention

When it happens

Trigger: PUT /api/models/active where `registry_round_trip` returns a reply JSON lacking both keys, or with `active_model: null`, indicating a registry implementation bug or protocol/version mismatch between gateway and registry.

Common situations: Running a newer gateway against an older registry capsule whose set_active reply schema differs (e.g. emits `{status:"ok"}` without active_model); a registry bug that swallows the bind result and serializes null; corrupted/reshaped reply after an intermediary transformation.

Related errors


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