herdrdev/herdr · error · io::Error
timed out reading stream frame header
Error message
timed out reading stream frame header
What it means
The pane graphics streaming code reads each frame's header line byte-by-byte with both an idle timeout and an absolute total deadline. If the total deadline passes while the header is still incomplete (or before it even starts), read_line returns ErrorKind::TimedOut with 'timed out reading stream frame header'. The idle deadline alone (no bytes at all) does not trigger it — only exceeding the overall budget does.
Source
Thrown at src/api/server/pane_graphics_stream.rs:405
loop {
if !stream_is_running(running, stream_active) {
return Ok(None);
}
ensure_before_deadlines(
idle_deadline,
total_deadline,
"timed out reading stream frame header",
)?;
match stream.read(&mut byte) {
Ok(0) => return Ok(None),
Ok(_) => {
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) => {View on GitHub (pinned to f457cff4f2)
Solutions
- Increase the total timeout budget for the stream read so slow producers fit.
- Investigate why the producer stalls mid-header (check the child process / upstream writer).
- If the stream is genuinely dead, treat this as EOF: close the stream and re-open the graphics session instead of looping.
- In tests, keep partial-header fixtures consistent with the configured deadline (first byte then silence must exceed total_timeout to reproduce deterministically).
Example fix
// before let line = read_line(&mut stream, idle, Duration::from_millis(100)).await?; // after let line = read_line(&mut stream, idle, Duration::from_secs(5)).await?;
Defensive patterns
Strategy: retry
Validate before calling
null
Try / catch
match read_line(&mut stream, idle, total).await {
Err(e) if e.kind() == io::ErrorKind::TimedOut => {
// header budget exhausted: close and reopen the graphics stream; do not reuse partial header bytes
}
other => other,
} Prevention
- Set the total header deadline generously relative to producer speed.
- Monitor producer health so stalled writers are killed instead of timing out readers.
- Discard partial state and resynchronize after any stream timeout.
When it happens
Trigger: serve_frames / read_response_line reading a graphics stream whose header line trickles in slowly enough that total elapsed time exceeds total_timeout; a producer that stalls partway through writing a header (e.g. `{"type":"...` then blocks); deadlines set too tight for slow producers in tests like partial_graphics_header_times_out_after_first_byte.
Common situations: A slow or hung child process emitting graphics/OSC sequences; heavy system load stretching a normally-fast producer past the deadline; misconfigured total_timeout in stream configuration or tests; pipe backpressure from a slow consumer upstream of the producer.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- timed out reading stream frame body
- stream frame header is too large
- stream ended mid-frame
- timed out reading api request
- timed out waiting for app response after {} ms
AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28).
Data as JSON: /api/errors/028df9a0fa5be91c.
Report an issue: GitHub.