zed-industries/zed · error
Token exchange failed
Error message
Token exchange failed (HTTP {}): {} What it means
This error is raised in `exchange_code` when the OAuth2 token endpoint responds with a non-2xx HTTP status. The library includes the HTTP status code and the (redacted) response body so the developer can see why the authorization-code exchange was rejected, while `redact_token_body` prevents tokens/secrets from leaking into logs. It wraps any server-side rejection of the code-for-token swap during `do_oauth_flow`.
Solutions
- Re-run the full OAuth flow to obtain a fresh authorization code — codes expire within minutes and are single-use
- Verify client_id, client_secret, and redirect_uri exactly match the values registered with the provider
- Log (redacted) response body to check the provider's error field, e.g. 'invalid_grant' vs 'invalid_client'
- Check for clock skew on the machine (NTP) if the provider validates code issuance time
- Retry later if the status is 5xx, as the provider may be temporarily unavailable
Example fix
// before
if !response.status().is_success() {
return Err(anyhow!("Token exchange failed (HTTP {}): {}", response.status(), redact_token_body(&body)));
}
// after
if !response.status().is_success() {
let reason = serde_json::from_str::<serde_json::Value>(&body)
.ok()
.and_then(|v| v.get("error").and_then(|e| e.as_str()).map(String::from));
return Err(anyhow!(
"Token exchange failed (HTTP {}): {} ({:?})",
response.status(),
redact_token_body(&body),
reason
));
} Defensive patterns
Strategy: try-catch
Validate before calling
// before starting the flow assert!(!auth_code.is_empty(), "authorization code is missing"); assert!(!client_secret.is_empty(), "client secret missing (check env/creds file)"); // redirect_uri must byte-match the one used in the authorize request assert_eq!(token_redirect_uri, authorize_redirect_uri);
Try / catch
match do_oauth_flow(cx).await {
Err(e) if e.to_string().contains("Token exchange failed (HTTP 5") => {
// provider outage: retry with backoff
}
Err(e) if e.to_string().contains("Token exchange failed") => {
// permanent rejection (invalid_grant/invalid_client): restart full OAuth flow
}
Err(e) => return Err(e),
Ok(creds) => Ok(creds),
} Prevention
- Treat authorization codes as single-use and short-lived; never cache or replay them
- Keep client_id/secret/redirect_uri in one config source so they cannot drift between authorize and token calls
- Sync system clock (NTP) on machines running the flow
- Redact token bodies in any logs you add around this code path
When it happens
Trigger: POSTing the authorization code, client_id, client_secret, and redirect_uri to the token endpoint and receiving a 400/401 (invalid code, expired code, code already used), a 403 (redirect_uri or client mismatch), or a 5xx from the auth server.
Common situations: Stale or replayed authorization codes (codes are single-use and short-lived), mismatched redirect_uri between the authorize and token requests, wrong client_id/client_secret in the environment, clock skew invalidating the code, or the IdP being temporarily down.
Related errors
- Failed to connect to Mistral API
- Token refresh failed
- Could not fetch Authorization Server Metadata for
- Could not fetch Protected Resource Metadata for
- {e}
AI-assisted analysis of zed-industries/zed@916fc2b8cb (2026-09-19).
Data as JSON: /api/errors/92b1fef18c72679e.
Report an issue: GitHub.
Appendix: source
Thrown at crates/x_ai_subscribed/src/x_ai_subscribed.rs:875
.append_pair("grant_type", "authorization_code")
.append_pair("client_id", CLIENT_ID)
.append_pair("code", code)
.append_pair("redirect_uri", redirect_uri)
.append_pair("code_verifier", verifier)
.finish();
let request = HttpRequest::builder()
.method(Method::POST)
.uri(XAI_TOKEN_URL)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(AsyncBody::from(body))?;
let mut response = client.send(request).await?;
let mut body = String::new();
smol::io::AsyncReadExt::read_to_string(response.body_mut(), &mut body).await?;
if !response.status().is_success() {
return Err(anyhow!(
"Token exchange failed (HTTP {}): {}",
response.status(),
redact_token_body(&body)
));
}
serde_json::from_str::<TokenResponse>(&body).context("Failed to parse token response")
}
async fn refresh_token(
client: &Arc<dyn HttpClient>,
refresh_token: &str,
) -> Result<TokenResponse, RefreshError> {
let body = form_urlencoded::Serializer::new(String::new())
.append_pair("grant_type", "refresh_token")
.append_pair("client_id", CLIENT_ID)
.append_pair("refresh_token", refresh_token)
.finish();View on GitHub (pinned to 916fc2b8cb)