{"record":{"id":"b53b84b9f5280de3","repo":"Hmbown/CodeWhale","slug":"sse-stream-idle-timeout-after-idle-secs-s-no-data-received-b53b84","errorCode":null,"errorMessage":"SSE stream idle timeout after {idle_secs}s — no data received (bytes_received={bytes}, stream_age_ms={age}, ms_since_last_event={since})","messagePattern":"SSE stream idle timeout after (.+?)s — no data received \\(bytes_received=(.+?), stream_age_ms=(.+?), ms_since_last_event=(.+?)\\)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/client/chat.rs","lineNumber":1478,"sourceCode":"            // A number of OpenAI-compatible providers omit `[DONE]` but send a\n            // terminal `finish_reason`. Either is valid terminal proof. A raw\n            // HTTP EOF with neither is not: treating that as MessageStop turns\n            // a truncated provider response into a successful empty turn.\n            let mut saw_finish_reason = false;\n            // Once an error has been emitted, do not follow it with a synthetic\n            // MessageStop (or a second, less-specific premature-EOF error).\n            let mut stream_failed = false;\n            // Set when a complete line or unterminated flush failed UTF-8.\n            // Skip further data-frame parsing so U+FFFD cannot enter the transcript.\n            let mut decode_failed = 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                        stream_failed = true;\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                        stream_failed = true;\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)> =","sourceCodeStart":1460,"sourceCodeEnd":1496,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/tui/src/client/chat.rs#L1460-L1496","documentation":"OpenAI-compatible streaming client idle-timeout: no bytes arrived on the SSE stream within the configured idle duration, so tokio_timeout returned Elapsed and the stream yields a diagnostic message including bytes received, stream age, and time since the last event, then aborts. This prevents a silently hung connection from stalling a turn indefinitely.","triggerScenarios":"handle_chat_completion_stream's byte_stream.next() times out: provider stops sending chunks mid-generation (overloaded server, dead upstream), a proxy holds the connection open without forwarding, or the model hangs before producing any output.","commonSituations":"Self-hosted/OpenAI-compatible backends (vLLM, Ollama, gateways) freezing under load; corporate proxies with aggressive SSE buffering; long tool-call or reasoning pauses exceeding the idle timeout.","solutions":["Increase stream_idle_timeout for providers/models known to pause between chunks.","Restart or scale the backend provider; check its logs for the stalled generation.","Check proxy/ingress (nginx, Envoy) read timeouts and disable response buffering for SSE.","Retry the request — the failure is surfaced to the caller's existing retry path."],"exampleFix":"// before\nclient = ChatClient::new().stream_idle_timeout(Duration::from_secs(60));\n// after: tolerate slow local backends\nclient = ChatClient::new().stream_idle_timeout(Duration::from_secs(600));","handlingStrategy":"retry","validationCode":"// sanity-check idle timeout for slow local backends\nassert!(idle >= Duration::from_secs(60), \"idle timeout too aggressive\");","typeGuard":null,"tryCatchPattern":"match item {\n    Err(e) if e.to_string().contains(\"idle timeout\") => {\n        warn!(\"provider stalled: {e}\");\n        retry_with_backoff();\n    }\n    other => handle(other)?,\n}","preventionTips":["Raise stream_idle_timeout for self-hosted backends (vLLM/Ollama)","Configure nginx/Envoy with proxy_buffering off and generous read timeouts","Watch backend GPU/queue saturation that stalls generations"],"tags":["network","sse","timeout","streaming"],"backgroundTag":"request-timeout","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T21:17:16.096Z"}