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
- Assemble the entire frame first, then send it immediately after connect via write_attribution_frame -- never split the preface across awaits.
- Flush the writer if you must buffer manually.
- 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
- Connect only when the token is already available; then write the whole preface at once.
- Never split the preface across awaits or leave it in an unflushed BufWriter.
- Bound retries (1-2 attempts); a client that stalls once will usually stall again.
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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Snapshot command timed out for {shell_name}
- unknown network proxy attribution token
- network proxy attribution environment mismatch
- empty proxy connection
- invalid network proxy attribution frame
AI-assisted analysis of openai/codex@339751715c (2026-08-25).
Data as JSON: /api/errors/6f857e6369375899.
Report an issue: GitHub.