googleworkspace/cli · error
Token refresh failed with status {}: {}
Error message
Token refresh failed with status {}: {} What it means
This error comes from refresh_token_with_reqwest(), the fallback refresh path used when yup-oauth2's hyper-based client fails (typically behind HTTP proxies). It POSTs grant_type=refresh_token with client_id, client_secret, and refresh_token to https://oauth2.googleapis.com/token and fires when the endpoint answers with a non-2xx status. The status code and raw response body are embedded in the message, so the underlying OAuth error (invalid_grant, invalid_client, etc.) is visible in the text.
Source
Thrown at crates/google-workspace-cli/src/auth.rs:72
let client = crate::client::shared_client().map_err(anyhow::Error::from)?;
let params = [
("client_id", client_id),
("client_secret", client_secret),
("refresh_token", refresh_token),
("grant_type", "refresh_token"),
];
let response = client
.post("https://oauth2.googleapis.com/token")
.form(¶ms)
.send()
.await
.context("Failed to send token refresh request")?;
if !response.status().is_success() {
let status = response.status();
let body = response_text_or_placeholder(response.text().await);
anyhow::bail!("Token refresh failed with status {}: {}", status, body);
}
let token_response: TokenResponse = response
.json()
.await
.context("Failed to parse token response")?;
Ok(token_response.access_token)
}
/// Returns the project ID to be used for quota and billing (sets the `x-goog-user-project` header).
///
/// Priority:
/// 1. `GOOGLE_WORKSPACE_PROJECT_ID` environment variable.
/// 2. `project_id` from the OAuth client configuration (`client_secret.json`).
/// 3. `quota_project_id` from Application Default Credentials (ADC).
pub fn get_quota_project() -> Option<String> {
// 1. Explicit environment variable (highest priority)View on GitHub (pinned to a3768d0e82)
Solutions
- Read the body in the message: invalid_grant means the refresh token is dead — run `gws auth logout && gws auth login` to mint a new one
- If the body says invalid_client, verify the saved client_secret.json / GOOGLE_WORKSPACE_CLI_CLIENT_ID/SECRET still match the GCP OAuth client that issued the token
- If status is 5xx or the body mentions proxy/TLS errors, retry after a short delay and check HTTP(S)_PROXY environment variables
- Confirm the account still has the required scopes granted and that the OAuth consent screen has not been reverted to testing with the user removed
Example fix
// before: treating every refresh failure identically
match refresh_token_with_reqwest(&id, &secret, &rt).await {
Ok(t) => t,
Err(_) => panic!("auth failed"),
}
// after: branch on the reported status/body to distinguish re-auth from transient failure
let msg = err.to_string();
if msg.contains("400") && msg.contains("invalid_grant") {
eprintln!("Session revoked — run `gws auth login` to re-authenticate");
} else if msg.contains("5") || msg.contains("503") {
// transient: safe to retry after backoff
} Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-flight: confirm the refresh token is present and non-empty before any call
fn has_refresh_token(cred: &serde_json::Value) -> bool {
cred.get("refresh_token").and_then(|v| v.as_str()).is_some_and(|s| !s.is_empty())
} Try / catch
match refresh_token_with_reqwest(&id, &secret, &rt).await {
Ok(tok) => { /* proceed */ }
Err(e) => {
let m = e.to_string();
if m.contains("invalid_grant") {
// terminal: prompt re-login, do NOT retry
} else if m.contains("invalid_client") {
// terminal: client credentials wrong, surface config error
} else {
// transient (5xx/proxy): retry with backoff, cap attempts
}
}
} Prevention
- Keep the OAuth client (client_id/secret) that issued a refresh token stable; re-issue tokens via login whenever the client changes
- Refresh tokens expire after ~6 months of non-use — schedule a periodic authenticated call in long-lived setups
- Set HTTP_PROXY/HTTPS_PROXY correctly so the reqwest fallback path uses the right proxy
- Log (never print) the token endpoint response body on failure — it distinguishes invalid_grant from invalid_client instantly
When it happens
Trigger: Google returns 400 invalid_grant when the stored refresh token was revoked, expired (6 months unused), or was issued to a different OAuth client; 401 invalid_client when client_id/client_secret no longer match the client that authorized the token (secret rotated or OAuth client deleted); 5xx on transient Google endpoint outages. Also triggered when a corporate proxy intercepts oauth2.googleapis.com and returns its own error page.
Common situations: User clicked 'Remove access' for the app in their Google Account security page; client_secret.json was replaced with credentials from a different GCP OAuth client than the one that produced the stored refresh token; refresh token unused for >6 months; proxy or TLS-inspection appliance rewriting the token endpoint response; system clock far in the past/future.
Related errors
- Token response contained no access token
- Failed to write client config: {e}
- Cannot read {}: {e}
- Invalid client_secret.json format: {e}
- No credentials found. Run `gws auth setup` to configure, `gw
AI-assisted analysis of googleworkspace/cli@a3768d0e82 (2026-08-16).
Data as JSON: /api/errors/5e27501914c8b37f.
Report an issue: GitHub.