xai-org/grok-build · error · OidcError
OidcError::TokenExchangeHttp
Error message
OidcError::TokenExchangeHttp
What it means
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.
Source
Thrown at crates/codegen/xai-grok-shell/src/auth/oidc/protocol.rs:431
crate::http::shared_client()
.post(token_endpoint)
.header("x-grok-client-version", xai_grok_version::VERSION)
.form(&[
("grant_type", "authorization_code"),
("code", code),
("redirect_uri", redirect_uri),
("client_id", client_id),
("code_verifier", code_verifier),
])
.timeout(std::time::Duration::from_secs(15)),
token_endpoint,
)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(anyhow::Error::new(OidcError::TokenExchangeHttp {
status,
body,
}));
}
Ok(resp.json().await?)
}
/// Retry gate for `refresh_tokens`. Defers to `classify_terminal` (the single
/// source of truth): only a recognized terminal code (`invalid_grant`,
/// `invalid_client`) stops retries. Everything else (5xx, 429, bare 4xx, or an
/// unrecognized/RFC-transient code) is retried.
fn is_transient_refresh_error(err: &anyhow::Error) -> bool {
let Some(OidcError::TokenRefreshHttp { status, body }) = err.downcast_ref::<OidcError>() else {
return true;
};
if *status >= 500 || *status == 429 {
return true;
}
let error_code = serde_json::from_str::<serde_json::Value>(body)View on GitHub (pinned to bc7f02eddd)
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)
Example fix
// before redirect_uri = "http://localhost:8080/callback" // registered: 127.0.0.1 // after redirect_uri = "http://127.0.0.1:8080/callback" // must match registration exactly
Defensive patterns
Strategy: try-catch
Validate before calling
// preflight sanity: confirm client registration basics before exchange
// (codes cannot be pre-validated; check redirect_uri and credentials instead)
fn assert_client_cfg(client_id: &str, client_secret: &str, redirect_uri: &str) -> Result<(), String> {
if client_id.is_empty() || client_secret.is_empty() { return Err("client credentials empty".into()); }
if !redirect_uri.starts_with("http://127.0.0.1") && !redirect_uri.starts_with("http://localhost") {
return Err("redirect_uri must match the loopback callback registered with the IdP".into());
}
Ok(())
} Try / catch
if let Err(e) = res {
if let Some(OidcError::TokenExchangeHttp { status, body }) = e.downcast_ref::<OidcError>() {
match body.as_str() {
b if b.contains("invalid_grant") => eprintln!("Code expired/used — restart login"),
b if b.contains("invalid_client") => eprintln!("Check client_id/client_secret"),
_ => eprintln!("Exchange failed: HTTP {status}: {body}"),
}
}
} Prevention
- 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)
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- OidcError::TokenRefreshHttp
- OIDC endpoint rejected request ({status}): {body}
- OidcError::CallbackAuthFailed
- OidcError::DiscoveryHttp
- send failed: {body}
AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31).
Data as JSON: /api/errors/dd8349d00533fc8e.
Report an issue: GitHub.