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

  1. Verify both sides use the same message schema and serde representation (same crate version, same IpcPayload enum)
  2. Ensure the peer always writes a 4-byte big-endian length prefix followed by exactly that many bytes of serde_json-serialized payload
  3. Log the raw frame bytes (buffered[4..frame_len]) alongside the serde error to identify the offending payload
  4. 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

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


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)