xai-org/grok-build · error · OidcError

OidcError::TokenRefreshHttp

Error message

OidcError::TokenRefreshHttp

What it means

OidcError::TokenRefreshHttp is raised when a refresh-token grant against the token endpoint returns a non-success HTTP status, carrying the status code and response body. The code also logs the oauth2 error code, the refresh-token suffix, client_id, and principal_type before returning this error. It means an existing session could not be renewed.

Source

Thrown at crates/codegen/xai-grok-shell/src/auth/oidc/protocol.rs:555

        token_endpoint,
    )
    .send()
    .await?;
    if !resp.status().is_success() {
        let status = resp.status().as_u16();
        let body = resp.text().await.unwrap_or_default();
        let error_code = serde_json::from_str::<serde_json::Value>(&body)
            .ok()
            .and_then(|v| v.get("error")?.as_str().map(str::to_owned));
        tracing::warn!(
            http_status = status,
            oauth2_error = ?error_code,
            rt_prefix = xai_grok_auth::bearer_suffix(refresh_token),
            client_id = %client_id,
            principal_type = ?principal_type,
            "OIDC: token refresh HTTP error"
        );
        return Err(anyhow::Error::new(OidcError::TokenRefreshHttp {
            status,
            body,
        }));
    }
    Ok(resp.json().await?)
}
#[derive(Debug, Deserialize)]
pub(super) struct IdTokenClaims {
    #[serde(default)]
    pub(super) sub: Option<String>,
    #[serde(default)]
    pub(super) email: Option<String>,
    #[serde(default)]
    pub(super) iss: Option<String>,
    #[serde(default)]
    pub(super) aud: Option<serde_json::Value>,
    #[serde(default)]
    pub(super) nonce: Option<String>,

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Re-run the login flow to obtain fresh tokens (invalid_grant cannot be refreshed)
  2. Check the logged oauth2_error code: invalid_grant/expired_token means re-auth, invalid_client means fix credentials
  3. If using token rotation, ensure only one client instance consumes each refresh token

Example fix

// before
// silently failing refresh loop with stale token
refresh(access_token.refresh_token.clone()).await?;
// after
match refresh(rt).await {
    Ok(t) => t,
    Err(_) => return run_login_flow().await, // force re-login on invalid_grant
}
Defensive patterns

Strategy: fallback

Validate before calling

// before refreshing, confirm a refresh token exists and is not empty
fn can_refresh(t: &SavedTokens) -> bool {
    !t.refresh_token.is_empty()
}

Try / catch

match refresh(tokens.clone()).await {
    Ok(fresh) => fresh,
    Err(e) if matches!(e.downcast_ref::<OidcError>(), Some(OidcError::TokenRefreshHttp { status: 400, .. })) => {
        // invalid_grant: refresh token dead — fall back to interactive login
        run_login_flow().await?
    }
    Err(e) => return Err(e), // transient: surface or retry with backoff
}

Prevention

When it happens

Trigger: The periodic/refresh path POSTs the refresh token to the token endpoint and gets a non-success status (commonly 400 invalid_grant), producing TokenRefreshHttp { status, body }.

Common situations: Refresh token revoked or expired (user logged out, password change, admin revocation), rotation consumed the old token, IdP policy changed, transient network/gateway errors.

Related errors


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