{"record":{"id":"00f212eb77f11cfd","repo":"Hmbown/CodeWhale","slug":"oauth-device-code-poll-failed-detail","errorCode":null,"errorMessage":"OAuth device-code poll failed ({detail})","messagePattern":"OAuth device-code poll failed \\((.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/oauth.rs","lineNumber":795,"sourceCode":"        .send()\n        .context(\"OAuth device-code poll failed\")?;\n    let (status, body): (_, OAuthTokenMaterial) =\n        parse_oauth_json(response, \"OAuth device-code poll\")?;\n    if status.is_success() && body.error.is_none() {\n        return Ok(DevicePollOutcome::Complete(body));\n    }\n    match body.error.as_deref().unwrap_or(\"\") {\n        \"authorization_pending\" => Ok(DevicePollOutcome::Pending),\n        \"slow_down\" => Ok(DevicePollOutcome::SlowDown {\n            interval_seconds: body.interval,\n        }),\n        _ => {\n            let detail = oauth_failure_detail(\n                body.error.as_deref(),\n                body.error_description.as_deref(),\n                status,\n            );\n            bail!(\"OAuth device-code poll failed ({detail})\");\n        }\n    }\n}\n\n/// Interactive device-code login for any provider whose row offers it.\n/// Prints the verification URL + user code to stderr and polls until\n/// approved. A provider with no device flow (ChatGPT) fails here with the\n/// reason, instead of deep in transport code.\npub async fn device_code_login(provider: OAuthProvider) -> Result<PendingOAuthLogin> {\n    // Endpoint resolution does blocking HTTP (discovery): it must run on the\n    // blocking worker, never on the async executor. Providers with no device\n    // flow fail here, before any thread spawns and before any network.\n    let params = oauth_provider_params(provider);\n    if params.device_code_path.is_none() {\n        bail!(\n            \"{} offers no device-code flow; sign in through the browser login instead\",\n            params.display_name\n        );","sourceCodeStart":777,"sourceCodeEnd":813,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/oauth.rs#L777-L813","documentation":"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).","triggerScenarios":"`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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// before polling, respect the provider interval\nawait sleep(Math.max(providedIntervalSecs, 5) * 1000);","typeGuard":null,"tryCatchPattern":"try {\n  let token = pollDeviceGrant(...);\n} catch (e) {\n  if (e.matches('authorization_pending')) { await sleep(interval); retry(); }\n  else if (e.matches('slow_down')) { interval += 5; await sleep(interval); retry(); }\n  else if (e.matches('expired_token') || e.matches('access_denied')) { restartLogin(); }\n  else { throw e; }\n}","preventionTips":["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"],"tags":["oauth","polling","http"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}