zeroclaw-labs/zeroclaw · error · anyhow::Error

Gemini CLI OAuth refresh failed (HTTP {status}): {body}

Error message

Gemini CLI OAuth refresh failed (HTTP {status}): {body}

What it means

refresh_gemini_cli_token POSTs the stored refresh token (plus optional client_id/client_secret) to Google's OAuth token endpoint. A non-2xx - most often 400 invalid_grant - is surfaced with the HTTP status and Google's response body so the exact OAuth error is visible.

Source

Thrown at crates/zeroclaw-providers/src/gemini.rs:392

                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
                    .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                    .with_attrs(::serde_json::json!({
                        "oauth_provider": "gemini_cli",
                        "phase": "refresh_request",
                        "error": format!("{}", error),
                    })),
                "gemini: CLI OAuth refresh request failed"
            );
            anyhow::Error::msg(format!("Gemini CLI OAuth refresh request failed: {error}"))
        })?;

    let status = response.status();
    let body = response
        .text()
        .unwrap_or_else(|_| "<failed to read response body>".to_string());

    if !status.is_success() {
        anyhow::bail!("Gemini CLI OAuth refresh failed (HTTP {status}): {body}");
    }

    #[derive(Deserialize)]
    struct TokenResponse {
        access_token: Option<String>,
        expires_in: Option<i64>,
    }

    let parsed: TokenResponse = serde_json::from_str(&body).map_err(|_| {
        ::zeroclaw_log::record!(
            ERROR,
            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
                .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                .with_attrs(::serde_json::json!({"oauth_provider": "gemini_cli"})),
            "gemini: CLI OAuth refresh response is not valid JSON"
        );
        anyhow::Error::msg("Gemini CLI OAuth refresh response is not valid JSON")
    })?;

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Re-run `gemini` in a terminal and complete OAuth so a fresh oauth_creds.json with a new refresh token is written
  2. Check myaccount.google.com/permissions and re-allow the Gemini CLI app if it was revoked
  3. If you use managed auth profiles, refresh or re-import the profile
  4. Verify the system clock (NTP) - significant skew can make Google reject refresh requests
Defensive patterns

Strategy: fallback

Validate before calling

// Detect a dead refresh token before it hits a chat request
async fn refresh_probe(token_state: &OAuthTokenState) -> bool {
    match refresh_gemini_cli_token(
        token_state.refresh_token.as_deref().unwrap_or(""),
        token_state.client_id.as_deref(),
        token_state.client_secret.as_deref(),
    ) {
        Ok(_) => true,
        Err(_) => false, // schedule re-login instead of failing mid-chat
    }
}

Try / catch

match provider.chat(/* ... */).await {
    Ok(resp) => Ok(resp),
    Err(e) if e.to_string().contains("OAuth refresh failed") => {
        // invalid_grant is terminal for this credential:
        // prompt the user to re-run `gemini` and retry once after re-auth
        prompt_relogin_and_retry_once().await
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: The refresh token was revoked (Google account password change, security reset, revoking Gemini CLI app access in account settings), expired, or does not match the client_id/client_secret sent with the request.

Common situations: Long-lived setups where Google expired the token; user revoked app access at myaccount.google.com/permissions; multiple Gemini CLI versions overwriting ~/.gemini credentials with different client ids; large system clock skew.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/7698da0cc3327341. Report an issue: GitHub.