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

  1. Read `body` in the error for the OAuth2 error code (e.g. invalid_grant, invalid_client) and fix the matching cause
  2. Verify client_id/client_secret and redirect_uri exactly match the IdP application registration
  3. 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

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


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/dd8349d00533fc8e. Report an issue: GitHub.