xai-org/grok-build · error
OIDC endpoint rejected request ({status}): {body}
Error message
OIDC endpoint rejected request ({status}): {body} What it means
When a refresh-token exchange against the OIDC token endpoint returns a non-success HTTP status, the provider wraps the status and response body into a `RefreshError`. The error is terminal if the status indicates an unrecoverable auth problem (e.g. 400/401 invalid_grant) via `is_terminal_auth_status`, and a server-provided `Retry-After` header is propagated when present.
Source
Thrown at crates/codegen/xai-grok-workspace/src/hub_auth/proactive.rs:692
Some(exp) if Utc::now() < exp => after.min(remaining_std(exp, Utc::now())),
Some(_) => after.min(RETRY_CAP),
None => after,
};
// `remaining_std` is zero at/after expiry; never replace backoff with 0.
bounded.max(RETRY_BASE)
}
/// `400`/`401`/`403` are terminal. `429`/`408`/`425`/`5xx` stay on the retry
/// backoff so a rate-limit cannot permanently stop `current()`.
async fn require_success(resp: reqwest::Response) -> Result<reqwest::Response, RefreshError> {
let status = resp.status();
if status.is_success() {
return Ok(resp);
}
let retry_after = parse_retry_after(&resp);
let body = resp.text().await.unwrap_or_default();
Err(RefreshError {
error: anyhow::anyhow!("OIDC endpoint rejected request ({status}): {body}"),
new_refresh_token: None,
terminal: is_terminal_auth_status(status),
retry_after,
})
}
fn remaining_std(exp: DateTime<Utc>, now: DateTime<Utc>) -> Duration {
(exp - now).to_std().unwrap_or(Duration::ZERO)
}
fn datetime_plus(dt: DateTime<Utc>, d: Duration) -> Option<DateTime<Utc>> {
chrono::TimeDelta::from_std(d)
.ok()
.and_then(|td| dt.checked_add_signed(td))
}
fn datetime_minus(dt: DateTime<Utc>, d: Duration) -> Option<DateTime<Utc>> {
chrono::TimeDelta::from_std(d)View on GitHub (pinned to bc7f02eddd)
Solutions
- If the status is 400/401 (invalid_grant), run `grok login` again — the refresh token chain is dead and cannot be recovered.
- For 429/5xx, honor the `Retry-After` value and retry later; these are transient.
- Verify oidc_issuer and oidc_client_id in auth.json match the values the refresh token was issued with.
- Check for corporate proxies/firewalls altering token endpoint traffic.
Defensive patterns
Strategy: try-catch
Try / catch
match provider.refresh().await {
Ok(tokens) => use_tokens(tokens),
Err(e) if e.terminal => {
// invalid_grant / revoked token: only re-login recovers
eprintln!("refresh token rejected (terminal): {} — run `grok login`", e.error);
prompt_grok_login()?;
}
Err(e) => {
// transient (429/5xx): respect server hint
let wait = e.retry_after.unwrap_or(Duration::from_secs(5));
tokio::time::sleep(wait).await;
retry_refresh()?;
}
} Prevention
- Never run the same account on multiple machines without sharing auth.json — refresh-token rotation invalidates stale copies.
- Honor Retry-After on 429 responses to avoid worsening rate limits.
- Verify oidc_issuer/oidc_client_id after any IdP migration or client rename.
- Monitor for terminal statuses and surface `grok login` to users instead of retrying forever.
When it happens
Trigger: POST to the issuer's token endpoint (refresh_token grant) returns 4xx/5xx: expired or revoked refresh token (invalid_grant), wrong client_id/client_secret, network-facing proxy errors (502/503), or rate limiting (429 with Retry-After).
Common situations: Refresh token rotated elsewhere (another machine) invalidating this copy; user revoked the session; issuer URL misconfigured pointing to the wrong host; corporate proxy returning HTML error pages; temporary IdP outage.
Related errors
- OIDC expires_in out of range: {secs}
- OidcError::DiscoveryHttp
- OidcError::TokenRefreshHttp
- send failed: {body}
- screen query failed: {body}
AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31).
Data as JSON: /api/errors/8b27c6cd39d21cf7.
Report an issue: GitHub.