openai/codex · error · io::Error

invalid network proxy attribution token length

Error message

invalid network proxy attribution token length

What it means

The 8-byte magic matched, but the u16 length prefix that follows was 0 or exceeded MAX_ATTRIBUTION_TOKEN_LEN (128 bytes). Rejected with io::ErrorKind::InvalidData. Almost always a writer-side framing bug: wrong endianness (the reader uses tokio's read_u16, i.e. big-endian/network order; a little-endian writer sending a token of length 3 emits 0x0300 = 768), a shifted stream, or garbage after the magic.

Source

Thrown at codex-rs/network-proxy/src/attribution.rs:96

        return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "empty proxy connection").into());
    }
    if marker[0] != ATTRIBUTION_FRAME_MAGIC[0] {
        return Ok(None);
    }

    let token = tokio::time::timeout(ATTRIBUTION_FRAME_TIMEOUT, async {
        let mut magic = [0_u8; ATTRIBUTION_FRAME_MAGIC.len()];
        stream.read_exact(&mut magic).await?;
        if &magic != ATTRIBUTION_FRAME_MAGIC {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "invalid network proxy attribution frame",
            ));
        }

        let token_len = stream.read_u16().await? as usize;
        if token_len == 0 || token_len > MAX_ATTRIBUTION_TOKEN_LEN {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "invalid network proxy attribution token length",
            ));
        }
        let mut token = vec![0_u8; token_len];
        stream.read_exact(&mut token).await?;
        String::from_utf8(token).map_err(|_| {
            io::Error::new(
                io::ErrorKind::InvalidData,
                "network proxy attribution token is not UTF-8",
            )
        })
    })
    .await
    .map_err(|_| {
        io::Error::new(
            io::ErrorKind::TimedOut,
            "network proxy attribution frame timed out",

View on GitHub (pinned to 339751715c)

Solutions

  1. Use write_attribution_frame, which writes (token.len() as u16).to_be_bytes(), or replicate big-endian u16 exactly.
  2. Write the whole frame with a single write_all so fields cannot interleave or shift.
  3. Keep the token within 1..=128 bytes so the length prefix is valid by construction.

Example fix

// before: little-endian length prefix
writer.write_all(&(token.len() as u16).to_le_bytes())?; // reader sees 0x0300 for len 3
// after: big-endian, matching read_u16 network order
writer.write_all(&(token.len() as u16).to_be_bytes())?;
Defensive patterns

Strategy: validation

Validate before calling

fn valid_attribution_frame_len(token: &str) -> bool {
    let n = token.len();
    n >= 1 && n <= 128 && u16::try_from(n).is_ok()
}

Type guard

fn valid_attribution_frame_len(token: &str) -> bool {
    (1..=128).contains(&token.len())
}

Try / catch

Match io::ErrorKind::InvalidData whose message contains 'token length': dump the raw prefix bytes and verify the u16 is big-endian and equals the token length; a shifted stream means the earlier write was partial.

Prevention

When it happens

Trigger: A hand-rolled frame writes the length little-endian or as u32 instead of big-endian u16; a truncated or interleaved first write shifts subsequent reads; the client sends the magic followed by unrelated bytes.

Common situations: Porting the preface to Python/Go where struct.pack/native little-endian is the default; test fixtures with hardcoded byte arrays drifting from the wire format; fuzzed input hitting the ingress.

Related errors


AI-assisted analysis of openai/codex@339751715c (2026-08-25). Data as JSON: /api/errors/180458353fbdf806. Report an issue: GitHub.