{"record":{"id":"2b3ef8f5266314c3","repo":"Hmbown/CodeWhale","slug":"sse-stream-idle-timeout-after-s-no-data-receiv","errorCode":null,"errorMessage":"SSE stream idle timeout after {}s — no data received (bytes_received={}, stream_age_ms={}, ms_since_last_chunk={})","messagePattern":"SSE stream idle timeout after (.+?)s — no data received \\(bytes_received=(.+?), stream_age_ms=(.+?), ms_since_last_chunk=(.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/client/anthropic.rs","lineNumber":325,"sourceCode":"            // UTF-8 char (CJK/emoji) split across two network reads is never\n            // corrupted to U+FFFD. Line boundaries ('\\n') are ASCII and can\n            // never fall inside a multi-byte sequence. (Mirrors chat.rs.)\n            let mut buffer: Vec<u8> = Vec::new();\n            let stream_start = std::time::Instant::now();\n            let mut last_chunk_at = std::time::Instant::now();\n            let mut bytes_received: usize = 0;\n            tokio::pin!(byte_stream);\n\n            loop {\n                let chunk = match tokio::time::timeout(stream_idle_timeout, byte_stream.next()).await {\n                    Ok(Some(Ok(chunk))) => chunk,\n                    Ok(Some(Err(e))) => {\n                        yield Err(anyhow::anyhow!(\"Stream read error: {e}\"));\n                        return;\n                    }\n                    Ok(None) => break,\n                    Err(_) => {\n                        yield Err(anyhow::anyhow!(super::stream_entry::idle_timeout_message(\n                            stream_idle_timeout,\n                            bytes_received,\n                            stream_start.elapsed(),\n                            last_chunk_at.elapsed(),\n                        )));\n                        return;\n                    }\n                };\n\n                bytes_received += chunk.len();\n                last_chunk_at = std::time::Instant::now();\n                buffer.extend_from_slice(&chunk);\n\n                loop {\n                    let line = match super::take_sse_line(&mut buffer) {\n                        Ok(Some(line)) => line,\n                        Ok(None) => break,\n                        Err(err) => {","sourceCodeStart":307,"sourceCodeEnd":343,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/client/anthropic.rs#L307-L343","documentation":"The Anthropic SSE reader enforces an idle timeout: if no bytes arrive within `stream_idle_timeout` (from `[tui] stream_chunk_timeout_secs`, default 900s, clamped 1-3600), the stream terminates with this diagnostics-rich message showing bytes received so far, total stream age, and time since the last chunk. It fires on the `Err(_)` arm of the `tokio::time::timeout` wrapper — the connection is healthy but silent.","triggerScenarios":"Extended server-side thinking gaps with no SSE keepalive/ping events; a stalled upstream between the provider and the client; proxies buffering and withholding chunks; streams paused so long the 900s default (or a user-lowered value) expires. Note `bytes_received=0` means nothing ever arrived after headers; a large value with a small `ms_since_last_chunk` gap means the stream ran long then stalled.","commonSituations":"Deep-reasoning models that go silent for many minutes; users lowering `stream_chunk_timeout_secs` to make failures snappier and then hitting it during legitimate long generations; corporate proxies that hold SSE responses; very long single completions on slow endpoints.","solutions":["Raise the idle budget in config: `[tui] stream_chunk_timeout_secs = 1800` (max 3600) if your model legitimately goes silent for long stretches.","Use the diagnostics fields: `bytes_received=0` → the server never started sending (open/first-token problem, check the model and endpoint); `bytes_received>0` → mid-stream stall (network/proxy issue).","For buffering proxies, bypass them for the Anthropic domain or force HTTP/1.1 (`CODEWHALE_FORCE_HTTP1=1`).","Retry the request — the stream is cleanly terminated and the turn can be reissued."],"exampleFix":"# before\n[tui]\nstream_chunk_timeout_secs = 60    # too tight for long thinking gaps\n# after\n[tui]\nstream_chunk_timeout_secs = 1800","handlingStrategy":"retry","validationCode":"// Config sanity check before long sessions: keep the idle budget generous.\nfn stream_idle_budget_ok(configured: Option<u64>) -> bool {\n    configured.unwrap_or(900) >= 900 // default 900s, clamp range 1..=3600\n}","typeGuard":"fn is_idle_timeout_error(e: &anyhow::Error) -> bool {\n    e.to_string().contains(\"SSE stream idle timeout\")\n}","tryCatchPattern":"match stream.next().await {\n    Some(Err(e)) if is_idle_timeout_error(&e) => {\n        let m = e.to_string();\n        if m.contains(\"bytes_received=0\") {\n            // server never started: wrong endpoint/model — do not blind-retry, diagnose\n            diagnose_first_token_path().await;\n        } else {\n            // mid-stream stall: safe to retry the turn once with backoff\n            retry_turn_once().await;\n        }\n    }\n    other => handle(other),\n}","preventionTips":["Set `[tui] stream_chunk_timeout_secs` (default 900, max 3600) above your model's longest silent thinking gap.","Use the diagnostics fields to route recovery: `bytes_received=0` is an open/first-token problem; `>0` is a mid-stream stall.","Do not blind-retry `bytes_received=0` cases — check model id and endpoint first."],"tags":["anthropic","streaming","timeout","sse","configuration"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}