Hmbown/CodeWhale · error
OAuth device-code poll failed
Error message
OAuth device-code poll failed ({detail}) What it means
Thrown when a single token-endpoint poll during the device grant returns an outcome that is not a pending state (`authorization_pending`, `slow_down`) and not a success — i.e. a terminal OAuth error. The detail comes from `oauth_failure_detail` (`error`, `error_description`, or HTTP status).
Solutions
- Read the `detail`: `access_denied` means the user refused — retry and approve the request
- `expired_token`: restart the device login and complete it faster
- `invalid_client`/`invalid_grant`: fix the provider's client credentials in config
- If polls were faster than the provider interval, respect the provider's `interval` and `slow_down` backoff
Defensive patterns
Strategy: retry
Validate before calling
// before polling, respect the provider interval await sleep(Math.max(providedIntervalSecs, 5) * 1000);
Try / catch
try {
let token = pollDeviceGrant(...);
} catch (e) {
if (e.matches('authorization_pending')) { await sleep(interval); retry(); }
else if (e.matches('slow_down')) { interval += 5; await sleep(interval); retry(); }
else if (e.matches('expired_token') || e.matches('access_denied')) { restartLogin(); }
else { throw e; }
} Prevention
- Always honor the provider's `interval` and `slow_down` backoff while polling
- Set a poll deadline slightly below the token expiry window
- Distinguish pending states from terminal errors before deciding to retry
When it happens
Trigger: `poll_device_grant` receives a terminal error from the token endpoint: `access_denied` (user rejected), `expired_token` (user took too long), `invalid_grant`, `invalid_client`, or any non-success HTTP status.
Common situations: User declined the login on their phone; the user code expired because polling took longer than the provider's interval (often due to `slow_down` backoff being ignored); provider rejects the client_id during token exchange.
Related errors
- MCP server rejected the request with and refreshing the…
- OAuth failed ( )
- OAuth returned HTTP that was not token JSON
- OIDC discovery failed with HTTP
- OAuth callback must be GET
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/00f212eb77f11cfd.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/oauth.rs:795
.send()
.context("OAuth device-code poll failed")?;
let (status, body): (_, OAuthTokenMaterial) =
parse_oauth_json(response, "OAuth device-code poll")?;
if status.is_success() && body.error.is_none() {
return Ok(DevicePollOutcome::Complete(body));
}
match body.error.as_deref().unwrap_or("") {
"authorization_pending" => Ok(DevicePollOutcome::Pending),
"slow_down" => Ok(DevicePollOutcome::SlowDown {
interval_seconds: body.interval,
}),
_ => {
let detail = oauth_failure_detail(
body.error.as_deref(),
body.error_description.as_deref(),
status,
);
bail!("OAuth device-code poll failed ({detail})");
}
}
}
/// Interactive device-code login for any provider whose row offers it.
/// Prints the verification URL + user code to stderr and polls until
/// approved. A provider with no device flow (ChatGPT) fails here with the
/// reason, instead of deep in transport code.
pub async fn device_code_login(provider: OAuthProvider) -> Result<PendingOAuthLogin> {
// Endpoint resolution does blocking HTTP (discovery): it must run on the
// blocking worker, never on the async executor. Providers with no device
// flow fail here, before any thread spawns and before any network.
let params = oauth_provider_params(provider);
if params.device_code_path.is_none() {
bail!(
"{} offers no device-code flow; sign in through the browser login instead",
params.display_name
);View on GitHub (pinned to 73e0f67d83)