{"record":{"id":"92b1fef18c72679e","repo":"zed-industries/zed","slug":"token-exchange-failed-http","errorCode":null,"errorMessage":"Token exchange failed (HTTP {}): {}","messagePattern":"Token exchange failed \\(HTTP (.+?)\\): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/x_ai_subscribed/src/x_ai_subscribed.rs","lineNumber":875,"sourceCode":"        .append_pair(\"grant_type\", \"authorization_code\")\n        .append_pair(\"client_id\", CLIENT_ID)\n        .append_pair(\"code\", code)\n        .append_pair(\"redirect_uri\", redirect_uri)\n        .append_pair(\"code_verifier\", verifier)\n        .finish();\n\n    let request = HttpRequest::builder()\n        .method(Method::POST)\n        .uri(XAI_TOKEN_URL)\n        .header(\"Content-Type\", \"application/x-www-form-urlencoded\")\n        .body(AsyncBody::from(body))?;\n\n    let mut response = client.send(request).await?;\n    let mut body = String::new();\n    smol::io::AsyncReadExt::read_to_string(response.body_mut(), &mut body).await?;\n\n    if !response.status().is_success() {\n        return Err(anyhow!(\n            \"Token exchange failed (HTTP {}): {}\",\n            response.status(),\n            redact_token_body(&body)\n        ));\n    }\n\n    serde_json::from_str::<TokenResponse>(&body).context(\"Failed to parse token response\")\n}\n\nasync fn refresh_token(\n    client: &Arc<dyn HttpClient>,\n    refresh_token: &str,\n) -> Result<TokenResponse, RefreshError> {\n    let body = form_urlencoded::Serializer::new(String::new())\n        .append_pair(\"grant_type\", \"refresh_token\")\n        .append_pair(\"client_id\", CLIENT_ID)\n        .append_pair(\"refresh_token\", refresh_token)\n        .finish();","sourceCodeStart":857,"sourceCodeEnd":893,"githubUrl":"https://github.com/zed-industries/zed/blob/916fc2b8cb3a815cbef4a3b40e13081be72036b6/crates/x_ai_subscribed/src/x_ai_subscribed.rs#L857-L893","documentation":"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`.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nif !response.status().is_success() {\n    return Err(anyhow!(\"Token exchange failed (HTTP {}): {}\", response.status(), redact_token_body(&body)));\n}\n// after\nif !response.status().is_success() {\n    let reason = serde_json::from_str::<serde_json::Value>(&body)\n        .ok()\n        .and_then(|v| v.get(\"error\").and_then(|e| e.as_str()).map(String::from));\n    return Err(anyhow!(\n        \"Token exchange failed (HTTP {}): {} ({:?})\",\n        response.status(),\n        redact_token_body(&body),\n        reason\n    ));\n}","handlingStrategy":"try-catch","validationCode":"// before starting the flow\nassert!(!auth_code.is_empty(), \"authorization code is missing\");\nassert!(!client_secret.is_empty(), \"client secret missing (check env/creds file)\");\n// redirect_uri must byte-match the one used in the authorize request\nassert_eq!(token_redirect_uri, authorize_redirect_uri);","typeGuard":null,"tryCatchPattern":"match do_oauth_flow(cx).await {\n    Err(e) if e.to_string().contains(\"Token exchange failed (HTTP 5\") => {\n        // provider outage: retry with backoff\n    }\n    Err(e) if e.to_string().contains(\"Token exchange failed\") => {\n        // permanent rejection (invalid_grant/invalid_client): restart full OAuth flow\n    }\n    Err(e) => return Err(e),\n    Ok(creds) => Ok(creds),\n}","preventionTips":["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"],"tags":["oauth","http","network","authentication"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"916fc2b8cb3a815cbef4a3b40e13081be72036b6","analyzedAt":"2026-09-19T19:09:50.599Z","contentChangedAt":"2026-09-19T19:09:50.599Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}