Hmbown/CodeWhale · error
Stream read error: {e}
Error message
Stream read error: {e} What it means
Mid-stream transport failure in the Anthropic SSE reader: the underlying `bytes_stream()` yielded `Err(e)` (a reqwest/hyper I/O error) after the response headers and body had begun flowing. This is distinct from the idle timeout (no error, just silence) and from API-level stream errors (which arrive as well-formed `error` events); the raw transport error is wrapped verbatim.
Source
Thrown at crates/tui/src/client/anthropic.rs:320
let stream = async_stream::stream! {
use futures_util::StreamExt;
// Raw byte buffer: decode only COMPLETE lines so a multi-byte
// UTF-8 char (CJK/emoji) split across two network reads is never
// corrupted to U+FFFD. Line boundaries ('\n') are ASCII and can
// never fall inside a multi-byte sequence. (Mirrors chat.rs.)
let mut buffer: Vec<u8> = Vec::new();
let stream_start = std::time::Instant::now();
let mut last_chunk_at = std::time::Instant::now();
let mut bytes_received: usize = 0;
tokio::pin!(byte_stream);
loop {
let chunk = match tokio::time::timeout(stream_idle_timeout, byte_stream.next()).await {
Ok(Some(Ok(chunk))) => chunk,
Ok(Some(Err(e))) => {
yield Err(anyhow::anyhow!("Stream read error: {e}"));
return;
}
Ok(None) => break,
Err(_) => {
yield Err(anyhow::anyhow!(super::stream_entry::idle_timeout_message(
stream_idle_timeout,
bytes_received,
stream_start.elapsed(),
last_chunk_at.elapsed(),
)));
return;
}
};
bytes_received += chunk.len();
last_chunk_at = std::time::Instant::now();
buffer.extend_from_slice(&chunk);
View on GitHub (pinned to 8880682c63)
Solutions
- Retry the request — transient transport resets usually succeed on a fresh connection.
- For repeated resets behind proxies/VPN, force HTTP/1.1 end-to-end (`CODEWHALE_FORCE_HTTP1=1`) to avoid H2 stream tearing.
- Check the wrapped error text: `connection reset`/`unexpected EOF` points at intermediary boxes; TLS errors point at certificate/proxy interception.
- On laptops, avoid suspending mid-generation; on unstable links prefer shorter outputs.
Defensive patterns
Strategy: retry
Try / catch
// In the stream consumer: a transport read error is terminal for this stream,
// but the turn can be retried on a fresh connection.
while let Some(ev) = stream.next().await {
match ev {
Ok(event) => handle(event),
Err(e) if e.to_string().starts_with("Stream read error") => {
notify_user_partial_output();
return retry_turn_with_backoff().await; // fresh connection
}
Err(e) => return Err(e),
}
} Prevention
- Retry turns whose stream dies mid-read — partial output plus a retry usually completes the work.
- Set `CODEWHALE_FORCE_HTTP1=1` in proxy-heavy environments to avoid HTTP/2 stream resets.
- Keep partial streamed text visible to the user so a retry is cheap (edit-from-partial rather than from scratch).
When it happens
Trigger: Connection reset by the server or an intermediary mid-generation; TLS session torn down; proxy/VPN dropping long-lived HTTP/2 streams; laptop sleep/resume during a long completion; NAT timeouts on idle-then-bursty streams.
Common situations: Corporate proxies or VPNs with aggressive idle connection reaping; HTTP/2 stream stalls behind middleboxes (the client already has an H2->HTTP/1.1 fallback at open time, but not mid-stream); flaky mobile/tethered networks during long generations.
Related errors
- SSE stream idle timeout after {}s — no data received (bytes_
- {err}
- Anthropic stream error ({error_type}): {message}
- SSE stream idle timeout after {}s — no data received (bytes_
- SSE stream idle timeout after {}s — no data received (bytes_
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/5f4f380f9ab65425.
Report an issue: GitHub.