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

  1. If the status is 400/401 (invalid_grant), run `grok login` again — the refresh token chain is dead and cannot be recovered.
  2. For 429/5xx, honor the `Retry-After` value and retry later; these are transient.
  3. Verify oidc_issuer and oidc_client_id in auth.json match the values the refresh token was issued with.
  4. 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

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


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