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

  1. Increase the total timeout budget proportional to the maximum expected frame size and slowest producer rate.
  2. Split very large frames into multiple smaller frames so each fits the deadline.
  3. Diagnose the producer's throughput (tracing, strace on write calls) — a healthy producer finishing a body should not approach the cap.
  4. 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

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

Related errors


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