{"record":{"id":"527e6fdcbf64db2c","repo":"Hmbown/CodeWhale","slug":"sse-stream-request-failed-after-http-1-1-fallback","errorCode":null,"errorMessage":"SSE stream request failed after HTTP/1.1 fallback: {err}. `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 failed after HTTP/1\\.1 fallback: (.+?)\\. `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":144,"sourceCode":"            || reqwest_error.is_timeout()\n            || reqwest_error.is_request();\n    }\n\n    should_retry_with_h1(policy, &format!(\"{err:#}\"))\n}\n\n/// Preserve provider-semantic failures returned by the H1 attempt. Only a\n/// transport failure should be normalized into the shared retryable network\n/// error; otherwise an auth or invalid-request failure could be retried as if\n/// switching protocols had failed.\nfn h1_fallback_error(err: anyhow::Error) -> anyhow::Error {\n    if let Some(llm_error) = err.downcast_ref::<LlmError>()\n        && !matches!(llm_error, LlmError::NetworkError(_) | LlmError::Timeout(_))\n    {\n        return err;\n    }\n\n    anyhow::Error::new(LlmError::NetworkError(format!(\n        \"SSE stream request failed after HTTP/1.1 fallback: {err}. \\\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    )))\n}\n\n/// Open an SSE response through the shared transport policy.\n///\n/// `attempt` builds and sends one wire-specific request on the client\n/// selected for the given policy (via [`client_for_policy`]); everything\n/// transport-shared lives here:\n///\n/// - the response-header wait is bounded by `open_req.open_timeout`;\n/// - a classified transport failure or header stall on the dual client\n///   retries exactly once on the HTTP/1.1 twin;\n/// - a failure on an already H1-pinned request never retries;\n/// - once response headers have been received the seam never retries —\n///   body/stream errors belong to the adapter's decode loop.","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/client/stream_entry.rs#L126-L162","documentation":"The SSE open path first tries the configured HTTP policy and, when the policy allows it, retries once with HTTP/1.1-only. If that H1 attempt fails at the transport level (a bare transport error, LlmError::NetworkError, or LlmError::Timeout), h1_fallback_error normalizes it into a typed LlmError::NetworkError with this message so the shared retry layer still recognizes it as retryable. Provider-semantic failures (auth, invalid request) downcast inside the guard are preserved un-normalized, never retried as transport problems.","triggerScenarios":"Streaming (SSE) chat requests through proxies/VPNs that break long-lived HTTP/2 streams and also degrade the HTTP/1.1 replay: TLS-intercepting middleboxes, HTTP/2 disabled or buggy on the gateway, connection resets mid-handshake. Non-streaming requests may still succeed, which is exactly why `codewhale doctor` can pass while streaming turns fail.","commonSituations":"Corporate proxy environments (mitm proxies, response buffering), Windows Schannel/proxy quirks, k8s ingresses without H2 streaming support, VPN or captive networks, remote-development egress filters.","solutions":["Set CODEWHALE_FORCE_HTTP1=1 (legacy alias DEEPSEEK_FORCE_HTTP1) and rerun codewhale to pin HTTP/1.1 from the first attempt","Check HTTP_PROXY/HTTPS_PROXY/NO_PROXY: make sure the provider API host either bypasses the proxy or traverses it correctly for streaming responses","Run `codewhale doctor` to confirm plain (non-streaming) requests work, isolating the fault to the streaming path","Verify streaming reachability from the same host with curl -N against the provider SSE endpoint","Let the shared retry layer retry — the error is typed NetworkError by design; if turns still die, capture and report the full {err} chain"],"exampleFix":"# before\ncodewhale   # SSE stream request failed after HTTP/1.1 fallback: ...\n\n# after\nexport CODEWHALE_FORCE_HTTP1=1\ncodewhale","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"match result {\n    Err(err) => {\n        if let Some(LlmError::NetworkError(msg)) = err.downcast_ref::<LlmError>() {\n            if msg.contains(\"HTTP/1.1 fallback\") {\n                // transport-class: safe to retry with backoff;\n                // suggest CODEWHALE_FORCE_HTTP1=1 after repeated failures\n            }\n        } else { return Err(err); } // auth/invalid-request: never retry\n    }\n    ok => ok?,\n}","preventionTips":["Pin CODEWHALE_FORCE_HTTP1=1 on networks known to break H2 streaming","Keep provider API hosts out of intercepting proxies (NO_PROXY) where policy allows","Run codewhale doctor after network changes to separate streaming vs non-streaming faults"],"tags":["network","sse","streaming","http2","proxy","retry"],"backgroundTag":"sse-stream-connection-failed","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}