{"record":{"id":"fe68146d9b9d5b8d","repo":"astrid-runtime/astrid","slug":"registry-reply-was-not-an-ipc-message","errorCode":null,"errorMessage":"registry reply was not an IPC message","messagePattern":"registry reply was not an IPC message","errorType":"http","errorClass":"GatewayError::Internal","httpStatus":500,"severity":"error","filePath":"crates/astrid-gateway/src/routes/models.rs","lineNumber":279,"sourceCode":"        .unwrap_or_else(tokio::time::Instant::now);\n    loop {\n        let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());\n        // No budget left (deadline already elapsed, or zero remaining): break\n        // to the timeout path rather than calling `recv(Some(ZERO))`, whose\n        // behaviour with a zero duration is implementation-defined. The\n        // absolute deadline above keeps total wait bounded regardless.\n        if remaining.is_zero() {\n            return Err(GatewayError::Internal(anyhow::anyhow!(\n                \"registry did not respond\"\n            )));\n        }\n        let Some(event) = reply_rx.recv(Some(remaining)).await else {\n            return Err(GatewayError::Internal(anyhow::anyhow!(\n                \"registry did not respond\"\n            )));\n        };\n        let AstridEvent::Ipc { message, .. } = &*event else {\n            return Err(GatewayError::Internal(anyhow::anyhow!(\n                \"registry reply was not an IPC message\"\n            )));\n        };\n        if !expected_source_ids.contains(&message.source_id) {\n            continue;\n        }\n        // Extract the guest-facing payload. Capsule `publish_json` arrives as\n        // `Custom { data }` when the JSON has no known IPC `type`; using the\n        // guest bytes unwraps that data instead of exposing the internal tagged\n        // wrapper to the HTTP API.\n        let value = registry_reply_payload_json(&message.payload)?;\n        // Skip a reply that belongs to a different concurrent same-principal\n        // SET (foreign `corr_id`); keep waiting within the remaining budget.\n        if reply_satisfies_corr_id(&value, corr_id) {\n            return Ok(value);\n        }\n    }\n}","sourceCodeStart":261,"sourceCodeEnd":297,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-gateway/src/routes/models.rs#L261-L297","documentation":"After receiving an event on the reply channel, `registry_round_trip` destructures it expecting `AstridEvent::Ipc`. Any other event variant triggers `GatewayError::Internal('registry reply was not an IPC message')`. This guards the invariant that only IPC messages should arrive on the registry reply subscription.","triggerScenarios":"The event bus delivers a non-IPC `AstridEvent` (e.g. a lifecycle, log, or control event) to the registry reply subscription — possible from over-broad topic subscriptions or a misbehaving bus publisher.","commonSituations":"Wildcard/overlapping topic subscriptions routing unrelated events to the reply channel; a bus implementation broadcasting control events to all subscribers; a test harness publishing raw events; version mismatch where event enums diverged between publisher and gateway.","solutions":["Log the received event variant to identify what is being published to the reply topic.","Narrow the reply subscription to the exact response topic/correlation key so unrelated events cannot arrive.","Align event enum versions between gateway and bus publishers.","If other variants are legitimately possible, handle/skip them instead of failing the round trip."],"exampleFix":"// before\nlet AstridEvent::Ipc { message, .. } = &*event else {\n    return Err(GatewayError::Internal(anyhow::anyhow!(\n        \"registry reply was not an IPC message\"\n    )));\n};\n// after\nlet AstridEvent::Ipc { message, .. } = &*event else {\n    tracing::debug!(event_kind = event.kind(), \"ignoring non-IPC event on reply topic\");\n    continue;\n};","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"fn as_ipc_event(event: &AstridEvent) -> Option<&IpcMessage> {\n    match event {\n        AstridEvent::Ipc { message, .. } => Some(message),\n        _ => None,\n    }\n}","tryCatchPattern":"match registry_round_trip(state, principal, req_topic, resp_topic, payload, corr).await {\n    Err(e) if e.message().contains(\"was not an IPC message\") => {\n        tracing::warn!(\"non-IPC event on reply topic; treating as transient\");\n        registry_round_trip(state, principal, req_topic, resp_topic, payload, corr).await\n    }\n    r => r,\n}","preventionTips":["Subscribe reply channels to exact topics, never wildcards.","Skip-and-continue on non-IPC events instead of aborting the round trip when subscriptions may be broad.","Keep AstridEvent enum versions aligned across publisher and gateway.","Add a debug log of unexpected event kinds for observability."],"tags":["ipc","event-bus","registry","gateway"],"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"}