Hmbown/CodeWhale · error
Stream read error: {e}
Error message
Stream read error: {e} What it means
reqwest/hyper returned an error while reading the response body mid-stream. The client walks the full `Error::source()` chain and logs it (as a `Stream read error` warning) together with transport-shape headers captured before the body was consumed — specifically so HTTP/2 RST_STREAM, chunked-encoding corruption, and gzip-compressor failure can be told apart when investigating issue #103 — then yields this error and stops the stream.
Source
Thrown at crates/tui/src/client/chat.rs:1332
// hyper / h2 / io error is visible — without this the
// outer "error decoding response body" message tells
// us nothing about WHY the stream died.
let mut error_chain = format!("{e}");
let mut current: Option<&(dyn std::error::Error + 'static)> =
std::error::Error::source(&e);
while let Some(source) = current {
error_chain.push_str(&format!(" -> {source}"));
current = std::error::Error::source(source);
}
crate::logging::warn(format!(
"Stream read error: {error_chain} \
(elapsed: {}ms, bytes_received: {}, ms_since_last_event: {}, headers: {})",
stream_start.elapsed().as_millis(),
bytes_received,
last_event_at.elapsed().as_millis(),
response_headers,
));
yield Err(anyhow::anyhow!("Stream read error: {e}"));
break;
}
};
bytes_received = bytes_received.saturating_add(chunk.len());
last_event_at = std::time::Instant::now();
byte_buf.extend_from_slice(&chunk);
// Guard against unbounded buffer growth (e.g., malformed stream without newlines)
const MAX_SSE_BUF: usize = 10 * 1024 * 1024; // 10 MB
if byte_buf.len() > MAX_SSE_BUF {
yield Err(anyhow::anyhow!("SSE buffer exceeded {MAX_SSE_BUF} bytes — aborting stream"));
break;
}
if byte_buf.len() > SSE_BACKPRESSURE_HIGH_WATERMARK {
tokio::time::sleep(Duration::from_millis(SSE_BACKPRESSURE_SLEEP_MS)).await;
}View on GitHub (pinned to 8880682c63)
Solutions
- Retry with backoff — most mid-body transport errors are transient
- Inspect the logged warning: the error chain and headers identify RST_STREAM vs chunked corruption vs gzip failure
- If RST_STREAM recurs, force HTTP/1.1 for this connection or bypass the proxy
- If gzip failure recurs, disable response compression on the client
Defensive patterns
Strategy: retry
Type guard
fn is_stream_read_error(err: &anyhow::Error) -> bool {
err.to_string().starts_with("Stream read error")
} Try / catch
match stream.next().await {
Some(Err(e)) if is_stream_read_error(&e) => {
// Check the warn log's source chain + headers first: RST_STREAM / chunked / gzip
backoff_retry_reopen().await;
}
Some(Err(e)) => return Err(e),
other => { /* forward */ }
} Prevention
- Retry mid-body transport errors with backoff before investigating
- Use the logged error chain and transport headers to classify RST_STREAM vs chunked corruption vs gzip failure
- Force HTTP/1.1 or disable response compression when the failure class repeats
When it happens
Trigger: Connection reset mid-stream; HTTP/2 RST_STREAM from an intermediary; chunked-encoding corruption from a proxy; response decompression (gzip) failure; captive-portal or middlebox interference.
Common situations: Long streams through proxies/CDNs that cut connections; flaky mobile networks; HTTP/2 interop bugs between hyper and certain servers.
Related errors
- Stream read error: {e}
- provider stream error: {error}
- Stream read error: {e}
- SSE stream idle timeout after {}s — no data received (bytes_
- Stream read error: {e}
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/18a25241169c05c7.
Report an issue: GitHub.