{"record":{"id":"5119e1793bd51a6c","repo":"astrid-runtime/astrid","slug":"registry-set-active-model-reply-carried-neither-an","errorCode":null,"errorMessage":"registry set_active_model reply carried neither an 'error' nor a non-null 'active_model' (an explicit-null 'active_model' is treated as absent)","messagePattern":"registry set_active_model reply carried neither an 'error' nor a non-null 'active_model' \\(an explicit-null 'active_model' is treated as absent\\)","errorType":"http","errorClass":"GatewayError::Internal","httpStatus":500,"severity":"error","filePath":"crates/astrid-gateway/src/routes/models.rs","lineNumber":533,"sourceCode":"        SET_ACTIVE_RESPONSE,\n        // The registry reads `model_id` (also accepted under `data`); the\n        // gateway forwards the raw `id` untouched and never interprets it.\n        // `corr_id` is echoed verbatim by the registry for reply correlation.\n        json!({ \"model_id\": body.id, \"corr_id\": corr_id }),\n        Some(corr_id.as_str()),\n    )\n    .await?;\n\n    // Success: `{ \"status\": \"ok\", \"active_model\": <entry> }` → return the\n    // persisted entry so the caller sees the canonical id the registry\n    // bound. Error: `{ \"error\": \"<msg>\" }` → 400 with the registry's\n    // message surfaced verbatim (the gateway does not reinterpret\n    // resolution / ambiguity errors). An explicit `null` active_model is a\n    // malformed reply, not a success — see `classify_set_active_reply`.\n    match classify_set_active_reply(&reply) {\n        SetActiveOutcome::Bound(active) => Ok(Json(active)),\n        SetActiveOutcome::Rejected(message) => Err(GatewayError::BadRequest(message)),\n        SetActiveOutcome::Malformed => Err(GatewayError::Internal(anyhow::anyhow!(\n            \"registry set_active_model reply carried neither an 'error' nor a \\\n             non-null 'active_model' (an explicit-null 'active_model' is treated \\\n             as absent)\"\n        ))),\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::{\n        GET_ACTIVE_REQUEST, GET_ACTIVE_RESPONSE, SetActiveOutcome, classify_set_active_reply,\n        registry_reply_payload_json, registry_round_trip, reply_satisfies_corr_id,\n    };\n    use std::sync::Arc;\n    use std::sync::atomic::{AtomicBool, Ordering};\n\n    use crate::error::GatewayError;\n    use crate::routes::WorkspaceContext;","sourceCodeStart":515,"sourceCodeEnd":551,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-gateway/src/routes/models.rs#L515-L551","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check gateway and registry capsule version compatibility and upgrade/downgrade so both use the same set_active reply schema.","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'.","Inspect `classify_set_active_reply` to confirm which reply shape was received, then patch the registry producer accordingly.","Add a regression test in the registry covering the explicit-null active_model case (mirroring the gateway's classify tests)."],"exampleFix":"// before (registry reply for success)\njson!({ \"status\": \"ok\", \"active_model\": null })\n// after\njson!({ \"status\": \"ok\", \"active_model\": bound_entry })","handlingStrategy":"validation","validationCode":"// validate the reply shape before treating it as a bind result\nlet ok = reply.get(\"active_model\").map_or(false, |v| !v.is_null())\n    || reply.get(\"error\").map_or(false, |v| v.is_string());\nif !ok {\n    return Err(anyhow!(\"malformed set_active_model reply: {reply}\"));\n}","typeGuard":"fn is_well_formed_set_active_reply(reply: &serde_json::Value) -> bool {\n    reply.get(\"error\").map_or(false, |v| v.is_string())\n        || reply.get(\"active_model\").map_or(false, |v| !v.is_null())\n}","tryCatchPattern":"match classify_set_active_reply(&reply) {\n    SetActiveOutcome::Bound(active) => Ok(Json(active)),\n    SetActiveOutcome::Rejected(msg) => Err(GatewayError::BadRequest(msg)),\n    SetActiveOutcome::Malformed => Err(GatewayError::Internal(anyhow!(\n        \"malformed set_active_model reply: {reply}\"\n    ))),\n}","preventionTips":["Pin gateway and registry capsule to protocol-compatible versions; add a contract test for the set_active reply schema","Have the registry log every reply it emits so malformed payloads are debuggable","Never emit explicit-null active_model on success; always include the bound entry or an error string"],"tags":["rust","gateway","registry","protocol-mismatch"],"backgroundTag":"unexpected-response-shape","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}