{"record":{"id":"dd8349d00533fc8e","repo":"xai-org/grok-build","slug":"oidcerror-tokenexchangehttp","errorCode":null,"errorMessage":"OidcError::TokenExchangeHttp","messagePattern":"OidcError::TokenExchangeHttp","errorType":"http","errorClass":"OidcError","httpStatus":null,"severity":"error","filePath":"crates/codegen/xai-grok-shell/src/auth/oidc/protocol.rs","lineNumber":431,"sourceCode":"        crate::http::shared_client()\n            .post(token_endpoint)\n            .header(\"x-grok-client-version\", xai_grok_version::VERSION)\n            .form(&[\n                (\"grant_type\", \"authorization_code\"),\n                (\"code\", code),\n                (\"redirect_uri\", redirect_uri),\n                (\"client_id\", client_id),\n                (\"code_verifier\", code_verifier),\n            ])\n            .timeout(std::time::Duration::from_secs(15)),\n        token_endpoint,\n    )\n    .send()\n    .await?;\n    if !resp.status().is_success() {\n        let status = resp.status().as_u16();\n        let body = resp.text().await.unwrap_or_default();\n        return Err(anyhow::Error::new(OidcError::TokenExchangeHttp {\n            status,\n            body,\n        }));\n    }\n    Ok(resp.json().await?)\n}\n/// Retry gate for `refresh_tokens`. Defers to `classify_terminal` (the single\n/// source of truth): only a recognized terminal code (`invalid_grant`,\n/// `invalid_client`) stops retries. Everything else (5xx, 429, bare 4xx, or an\n/// unrecognized/RFC-transient code) is retried.\nfn is_transient_refresh_error(err: &anyhow::Error) -> bool {\n    let Some(OidcError::TokenRefreshHttp { status, body }) = err.downcast_ref::<OidcError>() else {\n        return true;\n    };\n    if *status >= 500 || *status == 429 {\n        return true;\n    }\n    let error_code = serde_json::from_str::<serde_json::Value>(body)","sourceCodeStart":413,"sourceCodeEnd":449,"githubUrl":"https://github.com/xai-org/grok-build/blob/bc7f02eddd3d84085849dc19ed216f11c23b0571/crates/codegen/xai-grok-shell/src/auth/oidc/protocol.rs#L413-L449","documentation":"OidcError::TokenExchangeHttp is raised when the OAuth2 authorization-code token exchange returns a non-success HTTP status. The variant includes the status code and the raw response body (which usually contains the OAuth2 error code such as invalid_grant). It means the code-for-token swap at the token endpoint failed.","triggerScenarios":"After the OIDC callback delivers an authorization code, the token endpoint POST returns e.g. 400/401/500; TokenExchangeHttp { status, body } is returned from the exchange helper.","commonSituations":"Authorization code already used or expired (invalid_grant), mismatched client_id/client_secret, redirect_uri not exactly matching the registered one, clock skew, IdP outage.","solutions":["Read `body` in the error for the OAuth2 error code (e.g. invalid_grant, invalid_client) and fix the matching cause","Verify client_id/client_secret and redirect_uri exactly match the IdP application registration","Retry the login to get a fresh authorization code (codes are single-use and short-lived)"],"exampleFix":"// before\nredirect_uri = \"http://localhost:8080/callback\"  // registered: 127.0.0.1\n// after\nredirect_uri = \"http://127.0.0.1:8080/callback\"  // must match registration exactly","handlingStrategy":"try-catch","validationCode":"// preflight sanity: confirm client registration basics before exchange\n// (codes cannot be pre-validated; check redirect_uri and credentials instead)\nfn assert_client_cfg(client_id: &str, client_secret: &str, redirect_uri: &str) -> Result<(), String> {\n    if client_id.is_empty() || client_secret.is_empty() { return Err(\"client credentials empty\".into()); }\n    if !redirect_uri.starts_with(\"http://127.0.0.1\") && !redirect_uri.starts_with(\"http://localhost\") {\n        return Err(\"redirect_uri must match the loopback callback registered with the IdP\".into());\n    }\n    Ok(())\n}","typeGuard":null,"tryCatchPattern":"if let Err(e) = res {\n    if let Some(OidcError::TokenExchangeHttp { status, body }) = e.downcast_ref::<OidcError>() {\n        match body.as_str() {\n            b if b.contains(\"invalid_grant\") => eprintln!(\"Code expired/used — restart login\"),\n            b if b.contains(\"invalid_client\") => eprintln!(\"Check client_id/client_secret\"),\n            _ => eprintln!(\"Exchange failed: HTTP {status}: {body}\"),\n        }\n    }\n}","preventionTips":["Never retry with the same authorization code; it is single-use","Ensure redirect_uri is byte-identical to the IdP registration","Sync system clock (skew can invalidate tokens)"],"tags":["oidc","oauth2","http","token-exchange"],"backgroundTag":"token-exchange-failed","analyzedSha":"bc7f02eddd3d84085849dc19ed216f11c23b0571","analyzedAt":"2026-08-31T04:59:42.031Z","schemaVersion":2},"datasetVersion":"2026-08-31T09:17:48.483Z"}