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
- Re-run the login flow to obtain fresh tokens (invalid_grant cannot be refreshed)
- Check the logged oauth2_error code: invalid_grant/expired_token means re-auth, invalid_client means fix credentials
- 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
- Fall back to interactive login on 400 invalid_grant instead of looping
- Do not share/consume one refresh token from multiple sessions if the IdP rotates tokens
- Alert on refresh failures early rather than waiting for access-token expiry
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
- OIDC endpoint rejected request ({status}): {body}
- OidcError::TokenExchangeHttp
- OIDC expires_in out of range: {secs}
- OidcError::DiscoveryHttp
- send failed: {body}
AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31).
Data as JSON: /api/errors/292781699c77e280.
Report an issue: GitHub.