tinyhumansai/openhuman · error · anyhow::Error
MCP HTTP {} — {}
Error message
MCP HTTP {} — {} What it means
The main Streamable-HTTP JSON-RPC POST returned a non-2xx, non-401 status and the client bails with the status code and response body. (401 is handled separately and returned as a typed `McpUnauthorizedError` so the UI can surface 'needs authentication'.) This is the generic transport-level failure for HTTP MCP calls: `initialize`, `tools/list`, `tools/call` all land here on server rejection.
Source
Thrown at src/openhuman/mcp/http_client/client.rs:777
.get(reqwest::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
if status == reqwest::StatusCode::UNAUTHORIZED {
let resource_metadata = parse_www_authenticate_challenge(&headers)
.and_then(|challenge| challenge.resource_metadata);
// Return a TYPED error (not a string `bail!`) so callers can
// `downcast_ref::<McpUnauthorizedError>()` and surface an
// actionable "needs authentication" state (#3719) rather than a
// generic failure. `anyhow` preserves the root type through `?`.
return Err(anyhow::Error::new(McpUnauthorizedError {
endpoint: redact_endpoint(&self.endpoint),
resource_metadata,
}));
}
if !status.is_success() {
let text = response.text().await.unwrap_or_default();
anyhow::bail!("MCP HTTP {} — {}", status.as_u16(), text);
}
let payload: Value = if content_type.starts_with("text/event-stream") {
// Read the SSE body incrementally and return as soon as the single
// JSON-RPC reply frame arrives, instead of buffering to stream-close
// (`response.text().await`). A server that holds the stream open
// after replying would otherwise stall this call until the request
// timeout, and those stalls compound across a skill's tool chain
// (#4195). The request-level reqwest timeout still bounds the worst
// case (a server that never replies).
let mut raw: Vec<u8> = Vec::new();
let mut stream = response.bytes_stream();
let mut frame: Option<Value> = None;
while let Some(chunk) = stream.next().await {
raw.extend_from_slice(&chunk?);
// Decode the whole buffer each pass so a multi-byte UTF-8
// sequence split across chunk boundaries is never corrupted.
if let Some(data) = first_complete_sse_data(&String::from_utf8_lossy(&raw))? {View on GitHub (pinned to 7491200858)
Solutions
- Read the embedded status: 404 → fix the endpoint URL; 400 → inspect the request body claim in the message; 429 → back off and retry with jitter.
- For auth-related 403/expire, re-authenticate the server and retry.
- If the session was lost (often after a server restart), re-initialize to get a fresh `Mcp-Session-Id`.
- Retry idempotent calls once after a short backoff for transient 5xx.
Defensive patterns
Strategy: retry
Try / catch
match client.post_jsonrpc(/* ... */).await {
Err(e) if e.to_string().contains("MCP HTTP 429") => {
tokio::time::sleep(Duration::from_secs(2)).await;
client.post_jsonrpc(/* ... */).await // retry once after backoff
}
Err(e) if e.to_string().contains("MCP HTTP 404") => Err(e), // wrong endpoint: fix config
other => other,
} Prevention
- Validate the endpoint URL at config time with a cheap handshake call.
- Retry only idempotent operations; classify by status (429/5xx retry, 4xx fix config/auth).
- Re-initialize after server restarts instead of reusing stale session ids.
When it happens
Trigger: 404 from a wrong endpoint URL; 400 bad request from protocol/gateway validation; 403 forbidden from auth scopes; 5xx from the server or a proxy; 429 rate limiting; session-id invalid after server restart.
Common situations: Endpoint URL typo or missing `/mcp` path; expired bearer/short-lived token for servers without refresh; API rate limits on hosted MCP servers; upstream outage; cloudflare-style WAF blocking JSON-RPC POSTs.
Related errors
- [transport:local] HTTP ${response.status}: ${text || respons
- MCP notification {method} failed with {} — {}
- MCP error: {err}
- Socket not connected
- Cloud RPC returned an error
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/430360e0dea974e1.
Report an issue: GitHub.