Hmbown/CodeWhale · error · LlmError::NetworkError

SSE stream request failed after HTTP/1.1 fallback: {err}. `c

Error message

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`.

What it means

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.

Source

Thrown at crates/tui/src/client/stream_entry.rs:144

            || reqwest_error.is_timeout()
            || reqwest_error.is_request();
    }

    should_retry_with_h1(policy, &format!("{err:#}"))
}

/// Preserve provider-semantic failures returned by the H1 attempt. Only a
/// transport failure should be normalized into the shared retryable network
/// error; otherwise an auth or invalid-request failure could be retried as if
/// switching protocols had failed.
fn h1_fallback_error(err: anyhow::Error) -> anyhow::Error {
    if let Some(llm_error) = err.downcast_ref::<LlmError>()
        && !matches!(llm_error, LlmError::NetworkError(_) | LlmError::Timeout(_))
    {
        return err;
    }

    anyhow::Error::new(LlmError::NetworkError(format!(
        "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`."
    )))
}

/// Open an SSE response through the shared transport policy.
///
/// `attempt` builds and sends one wire-specific request on the client
/// selected for the given policy (via [`client_for_policy`]); everything
/// transport-shared lives here:
///
/// - the response-header wait is bounded by `open_req.open_timeout`;
/// - a classified transport failure or header stall on the dual client
///   retries exactly once on the HTTP/1.1 twin;
/// - a failure on an already H1-pinned request never retries;
/// - once response headers have been received the seam never retries —
///   body/stream errors belong to the adapter's decode loop.

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Set CODEWHALE_FORCE_HTTP1=1 (legacy alias DEEPSEEK_FORCE_HTTP1) and rerun codewhale to pin HTTP/1.1 from the first attempt
  2. Check HTTP_PROXY/HTTPS_PROXY/NO_PROXY: make sure the provider API host either bypasses the proxy or traverses it correctly for streaming responses
  3. Run `codewhale doctor` to confirm plain (non-streaming) requests work, isolating the fault to the streaming path
  4. Verify streaming reachability from the same host with curl -N against the provider SSE endpoint
  5. Let the shared retry layer retry — the error is typed NetworkError by design; if turns still die, capture and report the full {err} chain

Example fix

# before
codewhale   # SSE stream request failed after HTTP/1.1 fallback: ...

# after
export CODEWHALE_FORCE_HTTP1=1
codewhale
Defensive patterns

Strategy: retry

Try / catch

match result {
    Err(err) => {
        if let Some(LlmError::NetworkError(msg)) = err.downcast_ref::<LlmError>() {
            if msg.contains("HTTP/1.1 fallback") {
                // transport-class: safe to retry with backoff;
                // suggest CODEWHALE_FORCE_HTTP1=1 after repeated failures
            }
        } else { return Err(err); } // auth/invalid-request: never retry
    }
    ok => ok?,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/527e6fdcbf64db2c. Report an issue: GitHub.