astrid-runtime/astrid · error
wire frame is an object
Error message
wire frame is an object
What it means
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).
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.
Example fix
// before
frame.as_object_mut().expect("wire frame is an object").insert(
"principal".to_owned(),
serde_json::Value::String(principal.clone()),
);
// after
let obj = frame.as_object_mut().ok_or_else(|| UplinkError::InvalidFrameShape)?;
obj.insert("principal".to_owned(), serde_json::Value::String(principal.clone())); Defensive patterns
Strategy: type-guard
Validate before calling
// before inserting, narrow the frame
let Some(obj) = frame.as_object_mut() else {
return Err(UplinkError::InvalidFrameShape);
}; Type guard
fn is_json_object(v: &serde_json::Value) -> bool { v.is_object() } Try / catch
if !frame.is_object() {
return Err(UplinkError::InvalidFrameShape);
}
frame.as_object_mut().unwrap().insert("principal".to_owned(), serde_json::Value::String(principal.clone())); Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- invalid layout migration record
- package-managed method has an update command
- tool_describe payload is not valid JSON or has an…
- alice
- decode legacy log receipt
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/bcaac351dde053d6.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-uplink/src/native/mod.rs:530
Ok(())
}
async fn write_message(writer: &mut LocalWriteHalf, message: &IpcMessage) -> std::io::Result<()> {
let payload_bytes = message
.payload
.to_guest_bytes()
.map_err(|error| std::io::Error::other(format!("serialize IPC payload: {error}")))?;
let payload: serde_json::Value = serde_json::from_slice(&payload_bytes)
.map_err(|error| std::io::Error::other(format!("decode IPC payload: {error}")))?;
let mut frame = serde_json::json!({
"topic": message.topic,
"payload": payload,
"source_id": message.source_id,
});
if let Some(principal) = &message.principal {
frame
.as_object_mut()
.expect("wire frame is an object")
.insert(
"principal".to_owned(),
serde_json::Value::String(principal.clone()),
);
}
let bytes = serde_json::to_vec(&frame)
.map_err(|error| std::io::Error::other(format!("serialize IPC message: {error}")))?;
let len = u32::try_from(bytes.len())
.map_err(|_| std::io::Error::other("IPC message exceeds 4 GiB"))?;
tokio::time::timeout(WRITE_TIMEOUT, async {
writer.write_all(&len.to_be_bytes()).await?;
writer.write_all(&bytes).await?;
writer.flush().await
})
.await
.map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "IPC write timed out"))?
}
View on GitHub (pinned to affd8760f4)