herdrdev/herdr · error · io::Error
timed out reading stream frame body
Error message
timed out reading stream frame body
What it means
While reading a declared frame body, read_exact tracks an absolute total deadline. Each successful read resets the idle deadline, but once 'now' passes total_deadline the read aborts with ErrorKind::TimedOut and 'timed out reading stream frame body'. So a body that trickles in (never idling long enough to hit the idle timeout) still cannot exceed the overall budget — this is the absolute cap for slow-drip bodies (exercised by trickled_graphics_body_obeys_absolute_deadline).
Source
Thrown at src/api/server/pane_graphics_stream.rs:470
Some(idle_deadline),
Some(total_deadline),
"timed out reading stream frame body",
)?;
let remaining = len - data.len();
let read_len = remaining.min(chunk.len());
match stream.read(&mut chunk[..read_len]) {
Ok(0) if data.is_empty() => return Ok(None),
Ok(0) => {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"stream ended mid-frame",
));
}
Ok(n) => {
wait.on_progress();
let now = Instant::now();
if now >= total_deadline {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"timed out reading stream frame body",
));
}
data.extend_from_slice(&chunk[..n]);
idle_deadline = now + idle_timeout;
}
Err(err) if read_should_retry(&err) => {
wait.after_retry(Some(idle_deadline), Some(total_deadline));
}
Err(err) if is_connection_closed_error(&err) && data.is_empty() => return Ok(None),
Err(err) => return Err(err),
}
}
Ok(Some(data))
})
}View on GitHub (pinned to f457cff4f2)
Solutions
- Increase the total timeout budget proportional to the maximum expected frame size and slowest producer rate.
- Split very large frames into multiple smaller frames so each fits the deadline.
- Diagnose the producer's throughput (tracing, strace on write calls) — a healthy producer finishing a body should not approach the cap.
- On this error, abort the frame cleanly and reopen/resynchronize the stream; partial body data must be discarded.
Example fix
// before read_exact(&mut stream, len, idle, Duration::from_millis(500))?; // after read_exact(&mut stream, len, idle, Duration::from_secs(30))?;
Defensive patterns
Strategy: retry
Validate before calling
null
Try / catch
match read_exact(&mut stream, len, idle, total).await {
Err(e) if e.kind() == io::ErrorKind::TimedOut => {
// body budget exhausted: abort frame, discard partial bytes, reopen or renegotiate a smaller frame
}
other => other,
} Prevention
- Scale the total deadline with expected frame size; don't reuse header deadlines for bodies.
- Split large graphics payloads into multiple smaller frames.
- Abort cleanly on timeout — never reuse partial body data.
When it happens
Trigger: A producer that sends body bytes slower than the total deadline allows — e.g. one byte per poll interval for a body larger than the budget covers; a stalled producer that already sent the header; total_timeout configured too small relative to frame size and producer speed.
Common situations: Large graphics frames (screenshots, big Kitty image payloads) over a slow pipe or loaded machine; debug-rate-limited producers in tests; deadline values tuned for headers reused for multi-MB bodies; backpressure from a slow consumer making the producer pause.
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 header
- 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/b96cac63c7dfd54b.
Report an issue: GitHub.