{"record":{"id":"66062a39be7f19c4","repo":"zed-industries/zed","slug":"token-refresh-failed-http","errorCode":null,"errorMessage":"Token refresh failed (HTTP {}): {}","messagePattern":"Token refresh failed \\(HTTP (.+?)\\): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/x_ai_subscribed/src/x_ai_subscribed.rs","lineNumber":913,"sourceCode":"    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        .map_err(|e| RefreshError::Transient(e.into()))?;\n\n    let mut response = client\n        .send(request)\n        .await\n        .map_err(RefreshError::Transient)?;\n    let status = response.status();\n    let mut body = String::new();\n    smol::io::AsyncReadExt::read_to_string(response.body_mut(), &mut body)\n        .await\n        .map_err(|e| RefreshError::Transient(e.into()))?;\n\n    if !status.is_success() {\n        let err = anyhow!(\n            \"Token refresh failed (HTTP {}): {}\",\n            status,\n            redact_token_body(&body)\n        );\n        if status == http_client::StatusCode::BAD_REQUEST\n            || status == http_client::StatusCode::UNAUTHORIZED\n            || status == http_client::StatusCode::FORBIDDEN\n        {\n            return Err(RefreshError::Fatal(err));\n        }\n        return Err(RefreshError::Transient(err));\n    }\n\n    serde_json::from_str(&body).map_err(|e| RefreshError::Transient(e.into()))\n}\n\nfn extract_email_claim(jwt: &str) -> Option<String> {\n    let payload_b64 = jwt.split('.').nth(1)?;","sourceCodeStart":895,"sourceCodeEnd":931,"githubUrl":"https://github.com/zed-industries/zed/blob/916fc2b8cb3a815cbef4a3b40e13081be72036b6/crates/x_ai_subscribed/src/x_ai_subscribed.rs#L895-L931","documentation":"Raised in `refresh_token` when the refresh-token grant request to the OAuth provider returns a non-success HTTP status. The response body is redacted before inclusion so leaked credentials never appear in the error. Depending on the status, the caller classifies this as transient (retryable) or permanent (e.g. refresh token revoked, requiring re-authentication).","triggerScenarios":"Sending the refresh_token grant to the token endpoint and receiving 400/401 (invalid_grant — refresh token expired, rotated, or revoked) or 5xx/429 (provider outage or rate limiting).","commonSituations":"User revoked the app or the provider rotated/invalidated the refresh token, tokens idle past the provider's max lifetime, provider-side outages returning 5xx, hitting rate limits with too-frequent refreshes.","solutions":["Check the (redacted) body for 'invalid_grant' or 'invalid_client' — if present, discard stored tokens and restart the full OAuth flow","For 429/5xx statuses, retry with exponential backoff, since the code classifies these as transient","Confirm the refresh token in storage is current (rotation providers invalidate the old token on each use)","Check for concurrent refreshes racing each other and consuming rotated tokens; serialize refresh calls","Verify client credentials haven't changed on the provider dashboard"],"exampleFix":"// before\nlet err = anyhow!(\"Token refresh failed (HTTP {}): {}\", status, redact_token_body(&body));\n// after\nlet err = anyhow!(\"Token refresh failed (HTTP {}): {}\", status, redact_token_body(&body));\nif status.is_server_error() || status == http_client::StatusCode::TOO_MANY_REQUESTS {\n    return Err(RefreshError::Transient(err));\n}\nreturn Err(RefreshError::Fatal(err));","handlingStrategy":"retry","validationCode":"// before refreshing\nassert!(!refresh_token.expose_secret().is_empty(), \"no stored refresh token\");\n// skip refresh if access token still has >=60s lifetime\nif access_token_expires_at > now + Duration::from_secs(60) { return Ok(access_token); }","typeGuard":null,"tryCatchPattern":"match get_fresh_credentials(cx).await {\n    Ok(creds) => creds,\n    Err(RefreshError::Transient(e)) => {\n        // retry with exponential backoff for 429/5xx\n        backoff_retry(|| get_fresh_credentials(cx), 3).await?\n    }\n    Err(RefreshError::Fatal(e)) => {\n        // refresh token revoked/expired: clear stored tokens, prompt re-auth\n        clear_stored_tokens();\n        return Err(e);\n    }\n}","preventionTips":["Persist and use only the newest refresh token with rotating-token providers","Serialize refresh calls so concurrent requests can't consume a rotated token","Classify statuses up front: 400/401 = re-auth, 429/5xx = backoff retry","Re-authenticate proactively before the refresh token hits the provider's max idle lifetime"],"tags":["oauth","http","token-refresh","network"],"backgroundTag":"http-error-response","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"}