tinyhumansai/openhuman · error · McpUnauthorizedError
MCP unauthorized for `{}` (HTTP 401)
Error message
MCP unauthorized for `{}` (HTTP 401) What it means
The MCP HTTP client received HTTP 401 from a remote MCP server, meaning the stored OAuth/session credential was rejected or is missing/expired. The client parses the WWW-Authenticate challenge (RFC 9728 resource metadata) to support re-auth. This error maps to McpUnauthorizedError, which observability classifies as McpServerNeedsAuth so the UI can prompt re-authorization rather than showing a generic failure.
Source
Thrown at src/openhuman/mcp/http_client/client.rs:770
};
impl McpHttpClient {
async fn read_response(&self, response: reqwest::Response) -> anyhow::Result<ResponseEnvelope> {
let status = response.status();
let headers = response.headers().clone();
let content_type = headers
.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();View on GitHub (pinned to 7491200858)
Solutions
- Re-run the OAuth/connect flow for that MCP server from connections settings to obtain a fresh token
- Check the server's token expiry requirements and refresh-token rotation; revoke stale tokens server-side if needed
- Verify the resource-metadata URL in the WWW-Authenticate challenge is reachable if the client must discover the authorization server
Defensive patterns
Strategy: retry
When it happens
Trigger: Thrown at src/openhuman/mcp/http_client/client.rs:770 when the library encounters an invalid state.
Common situations: See trigger scenarios.
Understand the failure class
- HTTP status errors: handling 4xx and 5xx responses — how to handle 4xx and 5xx responses properly.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/5aaa83a776a8b39c.
Report an issue: GitHub.