openai/codex · error · io::Error

network proxy attribution frame timed out

Error message

network proxy attribution frame timed out

What it means

The connection's first byte was the NUL magic, so the ingress started reading a full attribution frame under tokio::time::timeout with ATTRIBUTION_FRAME_TIMEOUT = 3 seconds; the complete frame (8-byte magic + u16 length + token bytes) did not arrive in time and the error is io::ErrorKind::TimedOut. The client connected, sent the first byte(s), then stalled mid-preface.

Source

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

        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",
        )
    })??;

    Ok(Some(token))
}

/// Writes the trusted bridge preface consumed by the shared proxy ingress.
#[doc(hidden)]
pub fn write_attribution_frame(writer: &mut impl Write, token: &str) -> io::Result<()> {
    if token.is_empty() || token.len() > MAX_ATTRIBUTION_TOKEN_LEN {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "invalid network proxy attribution token length",
        ));
    }
    let token_len = u16::try_from(token.len()).map_err(|_| {

View on GitHub (pinned to 339751715c)

Solutions

  1. Assemble the entire frame first, then send it immediately after connect via write_attribution_frame -- never split the preface across awaits.
  2. Flush the writer if you must buffer manually.
  3. Treat the 3s window as fixed (a compile-time constant): fix the client's write pattern, do not try to tune the timeout.

Example fix

// before: preface split across awaits, magic sent first
stream.write_all(MAGIC).await?;
let token = fetch_token().await; // may stall past the 3s window
stream.write_all(&frame_body).await?;
// after: prepare first, then write the whole preface right after connect
let token = fetch_token().await;
write_attribution_frame(&mut stream, &token)?;
Defensive patterns

Strategy: retry

Try / catch

// TimedOut on the preface is transient: reconnect and resend promptly
if let Some(e) = err.downcast_ref::<io::Error>() {
    if e.kind() == io::ErrorKind::TimedOut
        && e.to_string().contains("attribution frame timed out")
    {
        stream.shutdown().await.ok();
        // bounded retry: dial again and write the FULL frame immediately
    }
}

Prevention

When it happens

Trigger: Writing the magic and then blocking before the rest (computing the token, awaiting something else, half-open TCP); splitting the preface across delayed flushes on a buffered writer; a genuine network stall or middlebox dropping the half-written connection.

Common situations: A client that connects early 'to save time' and writes the preface later; a BufWriter that is never flushed; NAT/idle-timeout killing the connection between the first byte and the remainder.

Understand the failure class

Related errors


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