{"record":{"id":"bcaac351dde053d6","repo":"astrid-runtime/astrid","slug":"wire-frame-is-an-object","errorCode":null,"errorMessage":"wire frame is an object","messagePattern":"wire frame is an object","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-uplink/src/native/mod.rs","lineNumber":530,"sourceCode":"    Ok(())\n}\n\nasync fn write_message(writer: &mut LocalWriteHalf, message: &IpcMessage) -> std::io::Result<()> {\n    let payload_bytes = message\n        .payload\n        .to_guest_bytes()\n        .map_err(|error| std::io::Error::other(format!(\"serialize IPC payload: {error}\")))?;\n    let payload: serde_json::Value = serde_json::from_slice(&payload_bytes)\n        .map_err(|error| std::io::Error::other(format!(\"decode IPC payload: {error}\")))?;\n    let mut frame = serde_json::json!({\n        \"topic\": message.topic,\n        \"payload\": payload,\n        \"source_id\": message.source_id,\n    });\n    if let Some(principal) = &message.principal {\n        frame\n            .as_object_mut()\n            .expect(\"wire frame is an object\")\n            .insert(\n                \"principal\".to_owned(),\n                serde_json::Value::String(principal.clone()),\n            );\n    }\n    let bytes = serde_json::to_vec(&frame)\n        .map_err(|error| std::io::Error::other(format!(\"serialize IPC message: {error}\")))?;\n    let len = u32::try_from(bytes.len())\n        .map_err(|_| std::io::Error::other(\"IPC message exceeds 4 GiB\"))?;\n    tokio::time::timeout(WRITE_TIMEOUT, async {\n        writer.write_all(&len.to_be_bytes()).await?;\n        writer.write_all(&bytes).await?;\n        writer.flush().await\n    })\n    .await\n    .map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, \"IPC write timed out\"))?\n}\n","sourceCodeStart":512,"sourceCodeEnd":548,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-uplink/src/native/mod.rs#L512-L548","documentation":"Panic from `frame.as_object_mut().expect(\"wire frame is an object\")` in `write_message` (crates/astrid-uplink/src/native/mod.rs:530), called by `forward_outbound`. The code builds a serde_json::Value from a literal JSON object and, when a principal is present, inserts an extra key; the expect asserts the frame really is a JSON object. It fires only if the frame construction invariant is broken (the literal is no longer an object).","triggerScenarios":"Only when the serde_json::Value built immediately above is not Value::Object — i.e. a refactor changed the frame literal to an array/string/null while the insert path remained, or the frame was reassigned/replaced before the principal insert.","commonSituations":"Post-refactor regressions in the wire-format code: someone changed the frame payload shape (e.g. switched to an array envelope or a new top-level wrapper) without updating the principal-insert branch.","solutions":["Inspect the frame construction lines just above: ensure the serde_json! literal is still an object ({...}).","Replace expect with `if let Some(obj) = frame.as_object_mut()` or match to fail loudly with context during development.","Add a unit test for messages carrying a principal so the object invariant is exercised.","If the wire format intentionally changed, serialize principal into the new shape instead of inserting a key."],"exampleFix":"// before\nframe.as_object_mut().expect(\"wire frame is an object\").insert(\n    \"principal\".to_owned(),\n    serde_json::Value::String(principal.clone()),\n);\n// after\nlet obj = frame.as_object_mut().ok_or_else(|| UplinkError::InvalidFrameShape)?;\nobj.insert(\"principal\".to_owned(), serde_json::Value::String(principal.clone()));","handlingStrategy":"type-guard","validationCode":"// before inserting, narrow the frame\nlet Some(obj) = frame.as_object_mut() else {\n    return Err(UplinkError::InvalidFrameShape);\n};","typeGuard":"fn is_json_object(v: &serde_json::Value) -> bool { v.is_object() }","tryCatchPattern":"if !frame.is_object() {\n    return Err(UplinkError::InvalidFrameShape);\n}\nframe.as_object_mut().unwrap().insert(\"principal\".to_owned(), serde_json::Value::String(principal.clone()));","preventionTips":["After any change to the frame literal, assert frame.is_object() in a unit test.","Cover the principal-present path in wire-format tests so the insert branch runs in CI.","Use as_object_mut().ok_or(...) instead of expect in production paths.","Keep frame construction and mutation adjacent — avoid reassigning frame between build and insert."],"tags":["json","serde","invariant","panic","wire-protocol"],"backgroundTag":"internal-invariant-violation","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}