{"record":{"id":"5e9f66f29ab0faef","repo":"Hmbown/CodeWhale","slug":"sse-stream-request-did-not-receive-response-header","errorCode":null,"errorMessage":"SSE stream request did not receive response headers after {}s. `codewhale doctor` can still pass when non-streaming requests work; on Windows or proxy networks, try `CODEWHALE_FORCE_HTTP1=1` and rerun `codewhale`.","messagePattern":"SSE stream request did not receive response headers after (.+?)s\\. `codewhale doctor` can still pass when non-streaming requests work; on Windows or proxy networks, try `CODEWHALE_FORCE_HTTP1=1` and rerun `codewhale`\\.","errorType":"exception","errorClass":"LlmError::NetworkError","httpStatus":null,"severity":"error","filePath":"crates/tui/src/client/stream_entry.rs","lineNumber":186,"sourceCode":"    F: Fn(StreamHttpPolicy) -> Fut,\n    Fut: Future<Output = Result<reqwest::Response>>,\n{\n    let fallback_reason = match tokio::time::timeout(\n        open_req.open_timeout,\n        attempt(open_req.policy),\n    )\n    .await\n    {\n        Ok(Ok(response)) => return Ok(response),\n        Ok(Err(err)) => {\n            if !should_retry_error_with_h1(open_req.policy, &err) {\n                return Err(err);\n            }\n            \"transport error before response headers\"\n        }\n        Err(_elapsed) => {\n            if open_req.policy == StreamHttpPolicy::Http1Only {\n                return Err(anyhow::Error::new(LlmError::NetworkError(format!(\n                    \"SSE stream request did not receive response headers after {}s. \\\n                         `codewhale doctor` can still pass when non-streaming requests work; \\\n                         on Windows or proxy networks, try `CODEWHALE_FORCE_HTTP1=1` and rerun `codewhale`.\",\n                    open_req.open_timeout.as_secs()\n                ))));\n            }\n            \"response-header timeout\"\n        }\n    };\n\n    // No response body exists yet, so switching protocols and replaying the\n    // request is safe. The policy guard above keeps this to exactly one retry.\n    let h1_req = open_req.clone().with_h1_only();\n    crate::logging::warn(format!(\n        \"SSE stream {fallback_reason}; retrying once with HTTP/1.1\"\n    ));\n    match tokio::time::timeout(h1_req.open_timeout, attempt(h1_req.policy)).await {\n        Ok(Ok(response)) => Ok(response),","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/client/stream_entry.rs#L168-L204","documentation":"When the stream policy is Http1Only (pinned via CODEWHALE_FORCE_HTTP1=1), the SSE open request must return response headers within open_timeout. If tokio::time::timeout elapses first, this typed LlmError::NetworkError is returned immediately with the elapsed seconds — there is no protocol fallback because HTTP/1.1 is already the floor. The message notes doctor can still pass because non-streaming requests may respond fine.","triggerScenarios":"Running with CODEWHALE_FORCE_HTTP1=1 against a server or proxy that accepts the TCP/TLS connection but stalls before sending response headers: buffering proxies, overloaded upstreams, middleware that drops long-lived streaming responses, or an open_timeout too small for the provider's slow first byte.","commonSituations":"Forced HTTP/1 plus a corporate proxy that buffers SSE; provider incidents with slow time-to-first-header; high-latency or throttled links; local mock servers that never respond.","solutions":["Unset CODEWHALE_FORCE_HTTP1 to let the client try HTTP/2 first (only pin H1 if H2 is confirmed broken on your network)","Bypass or reconfigure the proxy for the API host: disable response buffering / allow streaming for that destination","Compare with curl -N --http1.1 from the same environment to confirm the header stall is network-side, not client-side","If the provider is just slow to first byte, raise the stream open timeout rather than pinning protocols"],"exampleFix":"# before\nexport CODEWHALE_FORCE_HTTP1=1\ncodewhale   # did not receive response headers after Ns\n\n# after (H2 first; the automatic H1 fallback still covers H2-only breakage)\nunset CODEWHALE_FORCE_HTTP1\ncodewhale","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"match result {\n    Err(err) if matches!(err.downcast_ref::<LlmError>(), Some(LlmError::NetworkError(m)) if m.contains(\"did not receive response headers\")) => {\n        // header stall under Http1Only: retry with backoff a bounded number of times;\n        // if persistent, unset CODEWHALE_FORCE_HTTP1 or fix the proxy path\n    }\n    other => other?,\n}","preventionTips":["Only pin HTTP/1.1 (CODEWHALE_FORCE_HTTP1=1) when H2 streaming is confirmed broken","Ensure proxies allow long-lived streaming responses without buffering","Size the stream open timeout to the provider's realistic time-to-first-header"],"tags":["network","sse","timeout","streaming","http1"],"backgroundTag":"response-header-timeout","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}