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

  1. Check the producer process: it likely exited or crashed mid-write; capture its exit status and stderr.
  2. If you control the producer, ensure it writes the full body before closing and computes the length from the exact bytes written.
  3. 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.
  4. 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

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


AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28). Data as JSON: /api/errors/697cb62cd08f06d7. Report an issue: GitHub.