Kuberwastaken/claurst · error
refresh: HTTP
Error message
refresh: HTTP {} — {} What it means
Refreshing an MCP OAuth token via the refresh_token grant returned a non-success HTTP status. The library includes the status and response body so the provider's error (commonly invalid_grant) is visible. On this failure the cached token cannot be renewed and re-authorization is required.
Solutions
- Re-run the MCP OAuth login flow for the server to obtain a new refresh token
- Check the response body for invalid_grant and, if present, treat stored tokens as revoked
- Verify client_id/client_secret and token endpoint URL are still correct
- For 5xx responses, retry after confirming the provider is healthy
Defensive patterns
Strategy: fallback
Try / catch
match get_valid_mcp_token(server).await {
Err(e) if e.to_string().starts_with("refresh: HTTP") && e.to_string().contains("invalid_grant") => {
// clear stored tokens and trigger interactive re-auth
}
other => other?,
} Prevention
- Re-authenticate periodically; refresh tokens rotate and old ones get revoked
- Do not reuse refresh tokens across machines or sessions
- Monitor for invalid_grant in response bodies as a signal to re-auth
When it happens
Trigger: refresh_mcp_token (called by get_valid_mcp_token when the stored access token is expired) POSTs the refresh token and receives a non-2xx status.
Common situations: Refresh token revoked or expired (provider rotation invalidates old tokens); user revoked the app in provider settings; client credentials changed; token endpoint URL wrong or provider returning 5xx.
Related errors
- Bridge register: server returned
- Token exchange failed
- Token exchange failed
- API key creation failed
- exchange_code: HTTP
AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10).
Data as JSON: /api/errors/e4210e081b23ff24.
Report an issue: GitHub.
Appendix: source
Thrown at src-rust/crates/mcp/src/oauth.rs:544
.refresh_token
.as_deref()
.ok_or_else(|| anyhow::anyhow!("Token for {} has no refresh token", server_name))?
.to_string();
let client = reqwest::Client::new();
let params = [("grant_type", "refresh_token"), ("refresh_token", refresh.as_str())];
let resp = client
.post(token_endpoint)
.form(¶ms)
.send()
.await
.map_err(|e| anyhow::anyhow!("refresh: request failed: {}", e))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
anyhow::bail!("refresh: HTTP {} — {}", status, body);
}
#[derive(serde::Deserialize)]
struct TokenResponse {
access_token: String,
refresh_token: Option<String>,
expires_in: Option<u64>,
}
let tr: TokenResponse = resp.json().await?;
let expires_at = tr.expires_in.map(|s| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
+ s
});
View on GitHub (pinned to b0637c97ec)