Hmbown/CodeWhale · error · anyhow::Error

SSE stream request failed: HTTP

Error message

SSE stream request failed: HTTP {status}: {error_text}

What it means

The streaming (SSE) Chat Completions request was rejected by the provider with a non-success status before any stream could open. The sanitized error body is re-raised with the HTTP status; additionally, if the provider's error mentions `reasoning_content`, the offending message indices are logged to diagnose thinking-mode replay violations. Raised in `handle_chat_completion_stream` in `crates/tui/src/client/chat.rs` around line 1395.

Solutions

  1. Read the sanitized `error_text` — for `reasoning_content` complaints, check the logged violation indices and ensure replayed assistant turns carry `reasoning_content`.
  2. Retry with backoff for 429/5xx statuses.
  3. Verify API key and provider model access for 401/403/404.
  4. If thinking-mode replay keeps failing, disable thinking mode for the session or rebuild the offending turn's history.
Defensive patterns

Strategy: retry

Validate before calling

fn stream_open_precheck(api_key: &str, thinking_mode: bool, history: &[Value]) -> Result<(), String> {
    if api_key.trim().is_empty() { return Err("missing API key".into()); }
    if thinking_mode && history.iter().any(is_assistant_tool_call_without_reasoning) {
        return Err("history has assistant tool-call turns missing reasoning_content".into());
    }
    Ok(())
}

Try / catch

match client.create_message_stream(req).await {
    Err(e) if e.to_string().contains("SSE stream request failed") => {
        let s = extract_status(&e.to_string());
        if s == 429 || s >= 500 { retry_with_backoff().await }
        else if e.to_string().contains("reasoning_content") { repair_reasoning_replay(); retry_once() }
        else { Err(e) }
    }
    other => other,
}

Prevention

When it happens

Trigger: Any non-2xx response when opening the SSE stream: 400 where the provider demands `reasoning_content` on assistant tool-call turns (DeepSeek thinking-mode replay violations), 401/403 auth, 429 rate limit, 5xx provider failure on stream open.

Common situations: DeepSeek thinking mode replaying assistant tool-call turns without `reasoning_content` despite the sanitizer; expired keys; bursts of autocomplete streams hitting rate limits; provider incidents breaking stream establishment.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/cf5dc9aeec3acefa. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/client/chat.rs:1395

        let (response, stream_idle_timeout) = self.open_chat_stream_response(&url, &body).await?;

        let status = response.status();
        crate::client::record_provider_response(self.api_provider, status.as_u16());
        if !status.is_success() {
            let raw_error_text = bounded_error_text(response, ERROR_BODY_MAX_BYTES).await;
            let error_text = sanitize_http_error_body(
                Some(self.api_provider.display_name()),
                status.as_u16(),
                &raw_error_text,
            );
            // If DeepSeek rejected for missing reasoning_content despite the
            // sanitizer, dump the offending indices so we can diagnose where
            // they came from on the next failure.
            if error_text.contains("reasoning_content") {
                log_thinking_mode_violations(&body);
            }
            anyhow::bail!("SSE stream request failed: HTTP {status}: {error_text}");
        }

        let api_provider = self.api_provider;
        let base_url = self.base_url.clone();

        // Capture transport-shape headers before we consume `response` into
        // `bytes_stream()`. They are surfaced in the decode-error log path so
        // we can tell HTTP/2 RST_STREAM from chunked-encoding corruption from
        // gzip-compressor failure when investigating #103.
        let response_headers = format_stream_headers(response.headers());
        let byte_stream = response.bytes_stream();
        let configured_reasoning_stream_style = self.reasoning_stream_style.clone();

        let stream = async_stream::stream! {
            use futures_util::StreamExt;

            // Emit a synthetic MessageStart
            yield Ok(StreamEvent::MessageStart {

View on GitHub (pinned to 73e0f67d83)