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

  1. Retry the request — transient transport resets usually succeed on a fresh connection.
  2. For repeated resets behind proxies/VPN, force HTTP/1.1 end-to-end (`CODEWHALE_FORCE_HTTP1=1`) to avoid H2 stream tearing.
  3. Check the wrapped error text: `connection reset`/`unexpected EOF` points at intermediary boxes; TLS errors point at certificate/proxy interception.
  4. 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

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


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/5f4f380f9ab65425. Report an issue: GitHub.