Hmbown/CodeWhale · error

MCP Streamable HTTP rejected (transport=http url={} status={

Error message

MCP Streamable HTTP rejected (transport=http url={} status={}): {}

What it means

A streamable-HTTP send returned a non-success status that is neither stale-session (404 with session-expired body text) nor an incompatible endpoint (404/405/406 become StreamableSendError::Incompatible), so it lands in the generic Other bucket. The message includes the masked URL, the status, and a scrubbed body excerpt. Sibling variants StaleSession and Incompatible carry their own signals.

Source

Thrown at crates/tui/src/mcp/streamable_http.rs:104

            return Ok(());
        }

        if !status.is_success() {
            let body_excerpt = bounded_body_excerpt(response, ERROR_BODY_PREVIEW_BYTES).await;
            let stale_session = self.session_id.is_some()
                && is_streamable_http_stale_session_status(status, &body_excerpt);
            let body_excerpt = self.auth.server_error_preview(&body_excerpt);
            if stale_session {
                return Err(StreamableSendError::StaleSession(format!(
                    "status={status} body={body_excerpt}"
                )));
            }
            if is_streamable_http_incompatible_status(status) {
                return Err(StreamableSendError::Incompatible(format!(
                    "status={status} body={body_excerpt}"
                )));
            }
            return Err(StreamableSendError::Other(anyhow::anyhow!(
                "MCP Streamable HTTP rejected (transport=http url={} status={}): {}",
                mask_url_secrets(&self.url),
                status,
                body_excerpt,
            )));
        }

        let content_type = response
            .headers()
            .get(CONTENT_TYPE)
            .and_then(|value| value.to_str().ok())
            .map(str::to_string);
        // Reject an over-large declared body before reading anything (fast
        // path), then bound the read itself so chunked / length-less
        // responses cannot OOM us either — Content-Length alone does not
        // protect against a server that streams without declaring a length.
        if let Some(len) = response.content_length()
            && len > MAX_MCP_RESPONSE_BYTES as u64

View on GitHub (pinned to 8880682c63)

Solutions

  1. Read the status and body excerpt in the message — they carry the server's reason
  2. On 401/403: re-authenticate via MCP OAuth login and retry
  3. On 400: validate the request JSON-RPC payload against the server's expectations
  4. On 5xx: check server logs; retry with backoff after recovery
Defensive patterns

Strategy: try-catch

Type guard

```rust
// Prefer the typed variant when StreamableSendError is in scope:
fn as_streamable_error(err: &anyhow::Error) -> Option<&StreamableSendError> {
    err.downcast_ref::<StreamableSendError>()
}
// Otherwise match the message prefix:
fn is_streamable_rejected(msg: &str) -> bool {
    msg.starts_with("MCP Streamable HTTP rejected")
}
```

Try / catch

```rust
match send_streamable(&mut t, request).await {
    Err(e) => match e.downcast_ref::<StreamableSendError>() {
        Some(StreamableSendError::Incompatible(_)) => switch_transport_to_sse().await, // wrong endpoint
        Some(StreamableSendError::StaleSession(_)) => reinitialize(&mut t).await,     // fresh session
        _ => { log_status_and_body(&e); return Err(e); }                              // this error
    },
    ok => ok?,
}
```

Prevention

When it happens

Trigger: A request to the streamable /mcp endpoint answers 400 (invalid JSON-RPC), 401/403 (auth), 409, or 5xx — any rejection not classified as stale-session or wrong-endpoint.

Common situations: Expired OAuth token; Mcp-Session-Id header invalid after a server restart but the body not matching stale heuristics; server bugs on specific tools; gateway error pages replacing MCP responses.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/82ea1f210275097d. Report an issue: GitHub.