herdrdev/herdr · error · io::Error
stream frame header is too large
Error message
stream frame header is too large
What it means
While reading a graphics stream frame header, each byte is appended until newline; if the accumulated line exceeds max_bytes before the newline arrives, read_line rejects it with ErrorKind::InvalidData and 'stream frame header is too large'. This bounds header memory per frame. It almost always means the two sides disagree on framing — the reader is consuming what it thinks is a header but the writer is emitting something else (e.g. binary body bytes or a much longer line format).
Source
Thrown at src/api/server/pane_graphics_stream.rs:417
wait.on_progress();
let now = Instant::now();
let total_deadline_at =
*total_deadline.get_or_insert_with(|| now + total_timeout);
idle_deadline = Some(now + idle_timeout);
if now >= total_deadline_at {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"timed out reading stream frame header",
));
}
bytes.push(byte[0]);
if byte[0] == b'\n' {
return String::from_utf8(bytes)
.map(Some)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err));
}
if bytes.len() > max_bytes {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"stream frame header is too large",
));
}
}
Err(err) if read_should_retry(&err) => {
wait.after_retry(idle_deadline, total_deadline);
}
Err(err) if is_connection_closed_error(&err) => return Ok(None),
Err(err) => return Err(err),
}
}
})
}
fn read_exact(
stream: &mut LocalStream,
len: usize,View on GitHub (pinned to f457cff4f2)
Solutions
- Verify both reader and writer use the same stream framing protocol version (header line length cap and format).
- Log the first ~max_bytes of the accumulated line to see what is actually being read — it usually reveals body bytes or a different message format.
- After any earlier frame error, resynchronize by discarding until the next newline instead of continuing mid-stream.
- If legitimate headers now exceed the cap, raise max_bytes for read_line consistently on both sides.
Example fix
// before let header = read_line(&mut stream, idle, total, 128)?; // after let header = read_line(&mut stream, idle, total, 4096)?;
Defensive patterns
Strategy: validation
Validate before calling
// producer side: assert header fits the reader's cap before writing
let header = format!("{json}\n");
assert!(header.len() <= HEADER_MAX_BYTES, "header {} > cap {}", header.len(), HEADER_MAX_BYTES); Type guard
fn is_valid_header_line(line: &str) -> bool {
!line.is_empty() && line.len() <= HEADER_MAX_BYTES && line.ends_with('\n')
} Try / catch
match read_line(&mut stream, idle, total).await {
Err(e) if e.kind() == io::ErrorKind::InvalidData => {
// framing desync: dump the partial line, resync to next newline or reopen stream
}
other => other,
} Prevention
- Keep both sides on the same protocol version so header formats match.
- After any framing error, resynchronize instead of continuing mid-stream.
- Cap header JSON size at the producer; never emit unbounded header fields.
When it happens
Trigger: serve_frames parsing a stream where the expected header line never terminates within max_bytes: a protocol version mismatch emitting longer headers, a binary payload read as a header line, or a corrupt stream that lost the previous frame's newline.
Common situations: Upgrading one side of the graphics protocol but not the other (longer JSON headers); a desync after a partially-written earlier frame; a producer emitting raw image bytes where a header line was expected; tests with fixture headers exceeding the configured cap.
Related errors
- timed out reading stream frame header
- stream ended mid-frame
- timed out reading stream frame body
- server closed connection
- expected Welcome message
AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28).
Data as JSON: /api/errors/27528b323265809c.
Report an issue: GitHub.