zeroclaw-labs/zeroclaw · error
Google device code request failed ({}): {}
Error message
Google device code request failed ({}): {} What it means
Thrown by start_device_code_flow when POST https://oauth2.googleapis.com/device/code returns a non-success HTTP status AND the response body does not parse as a Google OAuth error JSON ({"error":..., "error_description":...}). It is the fallback branch: well-formed Google OAuth errors take the separate 'Google device code error' path, so this message means an opaque or non-JSON failure body. The exact status code and raw body are embedded in the message.
Source
Thrown at crates/zeroclaw-providers/src/auth/gemini_oauth.rs:230
.send()
.await
.context("Failed to start device code flow")?;
let status = response.status();
let body = response
.text()
.await
.context("Failed to read device code response")?;
if !status.is_success() {
if let Ok(err) = serde_json::from_str::<OAuthErrorResponse>(&body) {
anyhow::bail!(
"Google device code error: {} - {}",
err.error,
err.error_description.unwrap_or_default()
);
}
anyhow::bail!("Google device code request failed ({}): {}", status, body);
}
let device_response: DeviceCodeResponse =
serde_json::from_str(&body).context("Failed to parse device code response")?;
let user_code = device_response.user_code;
let verification_url = device_response.verification_url;
Ok(DeviceCodeStart {
device_code: device_response.device_code,
verification_uri_complete: Some(format!("{verification_url}?user_code={user_code}")),
user_code,
verification_uri: verification_url,
expires_in: device_response.expires_in.unwrap_or(1800),
interval: device_response.interval.unwrap_or(5),
})
}
View on GitHub (pinned to 88bb9c8533)
Solutions
- Read the embedded status and body in the message: a 4xx points to client credentials, a 5xx/429 to Google or the network path
- Verify oauth_client_id (and the alias table [providers.models.gemini.<profile>]) matches a valid Google Cloud OAuth client that permits the device flow
- If the body shows a proxy/interceptor page, bypass the proxy or allowlist oauth2.googleapis.com and retry
- On 429, wait for the quota window to reset before re-running the login
- Reproduce with curl -X POST https://oauth2.googleapis.com/device/code -d client_id=... -d scope=... to see the raw response
Example fix
# before (config.toml) — truncated/wrong client id [providers.models.gemini.default] oauth_client_id = "1234567890" oauth_client_secret = "..." # after — full, verified Google Cloud OAuth client id [providers.models.gemini.default] oauth_client_id = "1234567890-abcdefghijklmnop.apps.googleusercontent.com" oauth_client_secret = "GOCSPX-..."
Defensive patterns
Strategy: try-catch
Validate before calling
// Verify the alias carries a non-empty client id before starting the device flow.
let alias = config.providers.models.gemini.get(profile)
.ok_or_else(|| anyhow::anyhow!("missing [providers.models.gemini.{profile}]"))?;
if alias.oauth_client_id.as_deref().map_or(true, |s| s.trim().is_empty()) {
anyhow::bail!("oauth_client_id is empty; fix the alias config before device-code login");
} else {
start_device_code_flow(client, alias.oauth_client_id.as_deref().unwrap()).await?;
} Try / catch
match gemini_oauth::start_device_code_flow(client, client_id).await {
Ok(device) => device,
Err(e) => {
let msg = e.to_string();
if msg.contains("(429") || msg.contains("(5") {
// rate limit / server side: safe to retry after a pause
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
gemini_oauth::start_device_code_flow(client, client_id).await?
} else {
// 4xx with a non-OAuth body: credentials or proxy problem, do not retry blindly
return Err(e.context("device-code endpoint rejected the request; check client_id and network path"));
}
}
} Prevention
- Keep oauth_client_id/oauth_client_secret in [providers.models.gemini.<profile>] in sync with the Google Cloud OAuth app
- Smoke-test the endpoint with curl when the body looks like a proxy page
- Treat 429/5xx statuses as retryable and everything else as configuration failure
When it happens
Trigger: Running the Gemini device-code flow (auth login --model-provider gemini --device-code, which calls start_device_code_flow(client, client_id)) and the device-code endpoint replies non-2xx with a non-OAuth-JSON body: an HTML block page from a corporate proxy (403/502), a plain-text 429 rate-limit response, a Google outage page, or a malformed/typo'd client_id that makes the endpoint emit an unexpected payload.
Common situations: Typo in oauth_client_id under [providers.models.gemini.<profile>]; a Google Cloud OAuth app that is misconfigured or still in a publishing state that rejects the device flow; egress through a proxy that intercepts accounts.google.com/oauth2.googleapis.com; hitting token-endpoint quota after repeated logins.
Related errors
- Device code expired before authorization was completed
- User denied authorization
- Device code expired
- Gemini CLI OAuth refresh failed (HTTP {status}): {body}
- Gemini CLI OAuth token expired and no refresh_token availabl
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/0983f67dbd3f2ed1.
Report an issue: GitHub.