herdrdev/herdr · error · io::Error
stream ended mid-frame
Error message
stream ended mid-frame
What it means
After a frame header declares a body length, read_exact reads that many bytes. If the stream returns Ok(0) (EOF) after some body bytes were already received, it fails with ErrorKind::UnexpectedEof and 'stream ended mid-frame'. An EOF before any bytes of the frame is a clean end-of-stream (Ok(None)); EOF partway through is a truncated, corrupt frame instead.
Source
Thrown at src/api/server/pane_graphics_stream.rs:461
let mut chunk = vec![0_u8; STREAM_FRAME_BODY_CHUNK_BYTES.min(len)];
let total_deadline = Instant::now() + total_timeout;
let mut idle_deadline = Instant::now() + idle_timeout;
while data.len() < len {
if !stream_is_running(running, stream_active) {
return Ok(None);
}
ensure_before_deadlines(
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));View on GitHub (pinned to f457cff4f2)
Solutions
- Check the producer process: it likely exited or crashed mid-write; capture its exit status and stderr.
- If you control the producer, ensure it writes the full body before closing and computes the length from the exact bytes written.
- In the consumer, treat UnexpectedEof mid-frame as a corrupted stream: discard the partial frame and resynchronize or reopen the stream rather than using partial data.
- For tests, make fixtures either complete frames or EOF-before-any-body-bytes if a clean end is intended.
Example fix
// producer: before — header claims len, body partially flushed
write_all(format!("len={}\n", body.len()).as_bytes())?;
write_all(&body[..half])?; // crash here truncates frame
// after — write exactly the declared body, then flush
write_all(format!("len={}\n", body.len()).as_bytes())?;
write_all(&body)?;
flush()?; Defensive patterns
Strategy: try-catch
Validate before calling
null
Try / catch
match read_exact(&mut stream, len, idle, total).await {
Ok(None) => { /* clean end of stream */ }
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
// truncated frame: discard partial body, check producer exit status, reopen stream
}
other => other,
} Prevention
- Producers must write the declared body length exactly and flush before closing.
- Check child-process exit status before assuming stream corruption.
- Never render a partially received frame; always discard on mid-frame EOF.
When it happens
Trigger: The writer closes the pipe/socket after emitting a header (and possibly a partial body) without writing the full declared length — e.g. a crashed child process, a producer that miscomputes the length field, or a test stream that ends mid-body.
Common situations: Child process dies while flushing a graphics frame; producer writes header claiming N bytes but only sends fewer; abrupt termination (SIGKILL, panic) mid-write; fixtures in tests that truncate a frame body deliberately.
Related errors
- timed out reading stream frame header
- stream frame header is too large
- timed out reading stream frame body
- server closed connection
- api request line is too large
AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28).
Data as JSON: /api/errors/697cb62cd08f06d7.
Report an issue: GitHub.