{"record":{"id":"950bff478138fd69","repo":"zeroclaw-labs/zeroclaw","slug":"openai-oauth-token-request-failed-status-bod","errorCode":null,"errorMessage":"OpenAI OAuth token request failed ({status}): {body}","messagePattern":"OpenAI OAuth token request failed \\((.+?)\\): (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-providers/src/auth/openai_oauth.rs","lineNumber":396,"sourceCode":"\n    None\n}\n\npub fn extract_expiry_from_jwt(token: &str) -> Option<chrono::DateTime<Utc>> {\n    let payload = token.split('.').nth(1)?;\n    let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD\n        .decode(payload)\n        .ok()?;\n    let claims: serde_json::Value = serde_json::from_slice(&decoded).ok()?;\n    let exp = claims.get(\"exp\").and_then(|v| v.as_i64())?;\n    chrono::DateTime::<Utc>::from_timestamp(exp, 0)\n}\n\nasync fn parse_token_response(response: reqwest::Response) -> Result<TokenSet> {\n    if !response.status().is_success() {\n        let status = response.status();\n        let body = response.text().await.unwrap_or_default();\n        anyhow::bail!(\"OpenAI OAuth token request failed ({status}): {body}\");\n    }\n\n    let token: TokenResponse = response\n        .json()\n        .await\n        .context(\"Failed to parse OpenAI token response\")?;\n\n    let expires_at = token.expires_in.and_then(|seconds| {\n        if seconds <= 0 {\n            None\n        } else {\n            Some(Utc::now() + chrono::Duration::seconds(seconds))\n        }\n    });\n\n    Ok(TokenSet {\n        access_token: token.access_token,\n        refresh_token: token.refresh_token,","sourceCodeStart":378,"sourceCodeEnd":414,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-providers/src/auth/openai_oauth.rs#L378-L414","documentation":"The OpenAI token endpoint (auth.openai.com/oauth/token) returned a non-2xx response inside `parse_token_response`. This single chokepoint backs `exchange_code_for_tokens`, `refresh_access_token`, and device-code polling, so it fires for code exchange, token refresh, and device grants alike. The HTTP status and raw body are embedded; typical bodies are OAuth errors such as `invalid_grant` or `invalid_client`, or an HTML page from an intermediary.","triggerScenarios":"Exchanging an authorization code that expired (codes are short-lived and single-use) or was already consumed; refreshing with a revoked or rotated refresh_token; a PKCE code_verifier that does not match the challenge sent in the authorize URL; any 4xx/5xx from the endpoint.","commonSituations":"App crashed after exchanging but before persisting tokens — retry replays the used code; refresh token invalidated by a login on another machine; corporate proxy intercepting TLS and answering with an error page; clock skew.","solutions":["Read the embedded status and body: `invalid_grant` on refresh means the refresh token is dead — run a fresh interactive login; on code exchange, restart the flow for a new code","Use the same `PkceState` instance for `build_authorize_url` (challenge) and `exchange_code_for_tokens` (verifier)","Confirm redirect_uri is exactly `http://localhost:1455/auth/callback` (OPENAI_OAUTH_REDIRECT_URI)","If the body is HTML or status is 5xx/429, suspect the network path (proxy, outage) and retry with backoff"],"exampleFix":"// before: any failure aborts\nlet tokens = refresh_access_token(&client, &refresh).await?;\n\n// after: invalid_grant triggers re-login, transient failures retry\nlet tokens = match refresh_access_token(&client, &refresh).await {\n    Ok(t) => t,\n    Err(e) if e.to_string().contains(\"invalid_grant\") => run_interactive_login().await?,\n    Err(e) if e.to_string().contains(\"token request failed\") => retry_with_backoff().await?,\n    Err(e) => return Err(e),\n};","handlingStrategy":"try-catch","validationCode":"// avoid needless refresh calls: only refresh when expiry is close\nif let Some(exp) = extract_expiry_from_jwt(&tokens.access_token) {\n    if exp > Utc::now() + chrono::Duration::seconds(60) {\n        return Ok(tokens); // still valid, skip the token endpoint round-trip\n    }\n}","typeGuard":"fn is_openai_token_error(e: &anyhow::Error) -> bool {\n    e.to_string().contains(\"OpenAI OAuth token request failed\")\n}\n\nfn is_invalid_grant(e: &anyhow::Error) -> bool {\n    e.to_string().contains(\"invalid_grant\")\n}","tryCatchPattern":"let tokens = match refresh_access_token(&client, &refresh).await {\n    Ok(t) => t,\n    Err(e) if is_openai_token_error(&e) && is_invalid_grant(&e) => run_interactive_login().await?,\n    Err(e) if is_openai_token_error(&e) => retry_with_backoff(refresh_access_token(&client, &refresh)).await?,\n    Err(e) => return Err(e),\n};","preventionTips":["Exchange the authorization code exactly once, immediately after receiving it","Persist the new refresh token after every refresh — rotation invalidates the old one","Use one PkceState for URL construction and code exchange"],"tags":["oauth","openai","token-endpoint","http","rust"],"backgroundTag":"oauth-token-endpoint-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}