{"record":{"id":"d0358acdf54368a6","repo":"Hmbown/CodeWhale","slug":"sse-stream-idle-timeout-after-s-no-data-receiv-d0358a","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/chat.rs","lineNumber":1301,"sourceCode":"\n            // Telemetry for #103 stream-decode diagnostics: bytes received\n            // since the start of this stream and last successful event time.\n            // Surfaces in the error log when reqwest yields a chunk error so\n            // we can tell HTTP/2 RST_STREAM from chunk-decode-failure from\n            // gzip-corruption when investigating a flaky session.\n            let stream_start = std::time::Instant::now();\n            let mut last_event_at = std::time::Instant::now();\n            let mut bytes_received: usize = 0;\n            // Set when a `[DONE]` sentinel was seen, so the post-loop flush does\n            // not re-process trailing post-DONE bytes.\n            let mut saw_done = false;\n\n            'stream: loop {\n                let chunk_result = match tokio_timeout(idle, byte_stream.next()).await {\n                    Ok(Some(result)) => result,\n                    Ok(None) => break, // Stream ended normally\n                    Err(_elapsed) => {\n                        yield Err(anyhow::anyhow!(stream_idle_timeout_message(\n                            idle,\n                            bytes_received,\n                            stream_start.elapsed(),\n                            last_event_at.elapsed(),\n                        )));\n                        break;\n                    }\n                };\n                let chunk = match chunk_result {\n                    Ok(bytes) => bytes,\n                    Err(e) => {\n                        // Walk the error source chain so reqwest's underlying\n                        // hyper / h2 / io error is visible — without this the\n                        // outer \"error decoding response body\" message tells\n                        // us nothing about WHY the stream died.\n                        let mut error_chain = format!(\"{e}\");\n                        let mut current: Option<&(dyn std::error::Error + 'static)> =\n                            std::error::Error::source(&e);","sourceCodeStart":1283,"sourceCodeEnd":1319,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/client/chat.rs#L1283-L1319","documentation":"The tokio timeout wrapping `byte_stream.next()` elapsed with no chunk received inside the idle window, so the client yields this error with diagnostics: idle seconds, total bytes received, stream age, and time since the last chunk. These fields distinguish a stream that never produced a byte (server never started) from one that stalled mid-generation.","triggerScenarios":"Reasoning models that think for a long time before the first token, exceeding the idle window; a stalled proxy or network path; provider-side hang after 200 OK; idle timeout configured smaller than the provider's time-to-first-token.","commonSituations":"High-latency or mobile networks; deep-reasoning requests with long silent thinking phases; corporate proxies that idle-kill quiet connections.","solutions":["Retry the request — a silent stall is usually transient","Increase the stream idle timeout so it exceeds the model's expected time-to-first-token (reasoning models can think for tens of seconds silently)","If bytes_received=0 consistently, the server is not starting the response — verify the endpoint and request","Check for intermediaries (proxies, VPNs) that kill idle connections and bypass them"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"fn is_stream_idle_timeout(err: &anyhow::Error) -> bool {\n    err.to_string().starts_with(\"SSE stream idle timeout\")\n}","tryCatchPattern":"match stream.next().await {\n    Some(Err(e)) if is_stream_idle_timeout(&e) => {\n        backoff().await;\n        stream = client.reopen_stream(request).await?; // idempotent re-request\n    }\n    Some(Err(e)) => return Err(e),\n    other => { /* forward */ }\n}","preventionTips":["Set the stream idle timeout above the model's worst-case time-to-first-token (reasoning models think silently for a long time)","Use the diagnostics: bytes_received=0 means never-started, >0 with large ms_since_last_chunk means mid-stream stall","Keep-alive or bypass intermediaries that idle-kill quiet SSE connections"],"tags":["sse","timeout","network","streaming"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}