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 u64View on GitHub (pinned to 8880682c63)
Solutions
- Read the status and body excerpt in the message — they carry the server's reason
- On 401/403: re-authenticate via MCP OAuth login and retry
- On 400: validate the request JSON-RPC payload against the server's expectations
- 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
- Confirm the endpoint speaks streamable HTTP; sibling 404/405/406 (Incompatible) errors mean wrong endpoint/transport
- Re-auth after token revocation rather than assuming refresh will succeed
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
- MCP SSE POST rejected (transport=sse endpoint={} status={}):
- MCP session expired: {error}
- GET timeout
- MCP Streamable HTTP session expired; retry with a new sessio
- MCP SSE rejected (transport=http url={} status={}): {}
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/82ea1f210275097d.
Report an issue: GitHub.