Hmbown/CodeWhale · error · anyhow::Error

SSE stream idle timeout after

Error message

SSE stream idle timeout after {idle_secs}s — no data received (bytes_received={bytes}, stream_age_ms={age}, ms_since_last_chunk={since})

What it means

This error is raised in the Anthropic streaming client when no bytes arrive from the Messages API SSE stream for longer than the configured idle timeout (stream_idle_timeout). The tokio::time::timeout wrapping byte_stream.next() fires with Err(_), and the stream yields this diagnostic message with bytes_received, stream age, and time since the last chunk to aid debugging. It aborts the stream immediately rather than letting a stalled connection hang the turn.

Solutions

  1. Increase stream_idle_timeout in the client configuration if long pauses between chunks are expected for your model.
  2. Check Anthropic status / retry the request — a mid-stream stall is usually transient.
  3. Verify no proxy or VPN is buffering or dropping SSE connections; try a direct connection.
  4. Enable network-level keepalives or use a provider/proxy that sends SSE comments as heartbeats.

Example fix

// before: default timeout too short for extended thinking
let client = AnthropicClient::new().with_stream_idle_timeout(Duration::from_secs(30));
// after
let client = AnthropicClient::new().with_stream_idle_timeout(Duration::from_secs(300));
Defensive patterns

Strategy: retry

Validate before calling

// before the call: ensure timeout is sane
assert!(idle_timeout >= Duration::from_secs(30), "idle timeout too small for LLM streams");

Try / catch

// stream errors are anyhow::Error items on the stream
while let Some(item) = stream.next().await {
    match item {
        Ok(ev) => handle(ev),
        Err(e) if e.to_string().contains("idle timeout") => {
            warn!("stream stalled: {e}");
            retry_with_backoff();
        }
        Err(e) => return Err(e),
    }
}

Prevention

When it happens

Trigger: Any Anthropic streaming request where the HTTP connection stays open but delivers zero bytes for stream_idle_timeout: provider outage mid-stream, network path stalls (VPN, proxy keepalive drop), a hung model, or a proxy that accepts the request but never relays the response body.

Common situations: Corporate proxies buffering SSE indefinitely; long 'thinking' pauses exceeding the configured timeout; Anthropic API partial outages; mobile/unstable networks dropping the connection silently without TCP FIN.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/1732c13847a5d2d8. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/client/anthropic.rs:334

            let mut bytes_received: usize = 0;
            let mut ended = false;
            tokio::pin!(byte_stream);

            loop {
                if !ended {
                    match tokio::time::timeout(stream_idle_timeout, byte_stream.next()).await {
                        Ok(Some(Ok(chunk))) => {
                            bytes_received += chunk.len();
                            last_chunk_at = std::time::Instant::now();
                            buffer.extend_from_slice(&chunk);
                        }
                        Ok(Some(Err(e))) => {
                            yield Err(anyhow::anyhow!("Stream read error: {e}"));
                            return;
                        }
                        Ok(None) => ended = true,
                        Err(_) => {
                            yield Err(anyhow::anyhow!(super::stream_entry::idle_timeout_message(
                                stream_idle_timeout,
                                bytes_received,
                                stream_start.elapsed(),
                                last_chunk_at.elapsed(),
                            )));
                            return;
                        }
                    }
                }

                loop {
                    let line = match next_sse_line(&mut buffer, ended) {
                        Ok(Some(line)) => line,
                        Ok(None) => break,
                        Err(err) => {
                            yield Err(anyhow::anyhow!("{err}"));
                            return;
                        }

View on GitHub (pinned to 433685b202)