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
- Re-run `gemini` in a terminal and complete OAuth so a fresh oauth_creds.json with a new refresh token is written
- Check myaccount.google.com/permissions and re-allow the Gemini CLI app if it was revoked
- If you use managed auth profiles, refresh or re-import the profile
- 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
- Re-run `gemini` login on a schedule for long-lived services using CLI OAuth
- Do not auto-retry invalid_grant - it never succeeds without re-consent
- Keep client_id/client_secret consistent with the CLI version that minted the token
- Prefer API-key auth for unattended servers
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
- Google device code request failed ({}): {}
- Gemini CLI OAuth token expired and no refresh_token availabl
- Device code expired before authorization was completed
- User denied authorization
- Device code expired
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/7698da0cc3327341.
Report an issue: GitHub.