astrid-runtime/astrid · error
invalid IPC message
Error message
invalid IPC message: {error} What it means
read_message buffers 4-byte length-prefixed bytes from the local IPC stream and deserializes the frame body with serde_json. This error wraps any serde_json deserialization failure (InvalidData io::Error) when a frame's payload is not valid JSON or does not match the expected IpcPayload schema. It means the peer sent a malformed or unexpected message over the IPC channel.
Solutions
- Verify both sides use the same message schema and serde representation (same crate version, same IpcPayload enum)
- Ensure the peer always writes a 4-byte big-endian length prefix followed by exactly that many bytes of serde_json-serialized payload
- Log the raw frame bytes (buffered[4..frame_len]) alongside the serde error to identify the offending payload
- Check for protocol version mismatch after upgrading either binary; pin both to the same version
Example fix
// before: writing a bare payload without a length prefix stream.write_all(serde_json::to_vec(&msg)?).await?; // after: write 4-byte big-endian length then payload let bytes = serde_json::to_vec(&msg)?; stream.write_all(&(bytes.len() as u32).to_be_bytes()).await?; stream.write_all(&bytes).await?;
Defensive patterns
Strategy: try-catch
Validate before calling
// before reading, validate peer frames at the write site
fn assert_frame(msg: &[u8]) -> io::Result<()> {
serde_json::from_slice::<serde_json::Value>(msg)
.map(|_| ())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
} Type guard
fn is_valid_ipc_frame(bytes: &[u8]) -> bool {
serde_json::from_slice::<serde_json::Value>(bytes).is_ok()
} Try / catch
match read_message(&mut reader).await {
Ok(Some(msg)) => handle(msg),
Ok(None) => {/* clean EOF */},
Err(e) if e.kind() == io::ErrorKind::InvalidData => {
log::error!("malformed IPC frame: {e}");
// resync or restart the peer connection
}
Err(e) => return Err(e.into()),
} Prevention
- Share the exact message type crate between both IPC endpoints
- Always frame messages with a 4-byte big-endian length prefix
- Add integration tests that round-trip every IpcPayload variant through the framer
- Version the protocol and negotiate it on connect
When it happens
Trigger: Peer writes a frame whose body is truncated JSON, non-UTF8-ish invalid JSON, or a JSON value that fails to deserialize into the expected message type (wrong/missing fields, wrong enum variant); a length prefix not matching actual payload content; a non-Rust or versioned peer speaking a different protocol version.
Common situations: Version skew between the uplink binary and the native child process after upgrading one side; a hand-written test harness or debugger writing raw bytes into the IPC socket; frame corruption from writing partial frames without the 4-byte big-endian length prefix; logging or tracing code accidentally interleaving text into the stream.
Understand the failure class
Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.
Related errors
- registry reply not JSON
- Admin request timed out after
- an Astrid daemon appears to be running but its uplink is…
- an Astrid daemon appears to be running but its uplink is…
- an Astrid daemon is recorded as running (PID file) but its…
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/6323ea44776ea6ad.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-uplink/src/native/framing.rs:47
if self.buffered.len() >= 4 {
let len = u32::from_be_bytes(
self.buffered[..4]
.try_into()
.expect("four-byte frame prefix"),
) as usize;
if len > MAX_FRAME_BYTES {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("IPC frame too large: {len} bytes"),
));
}
let frame_len = 4_usize.checked_add(len).ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "IPC frame overflow")
})?;
if self.buffered.len() >= frame_len {
let message =
serde_json::from_slice(&self.buffered[4..frame_len]).map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("invalid IPC message: {error}"),
)
})?;
self.buffered.drain(..frame_len);
return Ok(Some(message));
}
}
let mut chunk = [0_u8; 8192];
let read = self.reader.read(&mut chunk).await?;
if read == 0 {
if self.buffered.is_empty() {
return Ok(None);
}
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"local IPC stream ended within a frame",View on GitHub (pinned to affd8760f4)