{"record":{"id":"7853529b29fa901f","repo":"Hmbown/CodeWhale","slug":"oauth-device-code-request-returned-success-without-a-device","errorCode":null,"errorMessage":"OAuth device-code request returned success without a device and user code","messagePattern":"OAuth device-code request returned success without a device and user code","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/oauth.rs","lineNumber":754,"sourceCode":"        let detail = oauth_failure_detail(\n            body.error.as_deref(),\n            body.error_description.as_deref(),\n            status,\n        );\n        bail!(\"OAuth device-code request failed ({detail})\");\n    }\n    if body\n        .device_code\n        .as_deref()\n        .is_some_and(|code| !code.trim().is_empty())\n        && body\n            .user_code\n            .as_deref()\n            .is_some_and(|code| !code.trim().is_empty())\n    {\n        return Ok(body);\n    }\n    bail!(\"OAuth device-code request returned success without a device and user code\");\n}\n\n/// Poll the token endpoint once, classifying the RFC 8628 outcome. Matches\n/// the legacy per-provider poll so the ported tests pin identical behavior.\nfn poll_device_grant(\n    token_endpoint: &str,\n    client_id: &str,\n    device_code: &str,\n) -> Result<codewhale_config::device_code::DevicePollOutcome<OAuthTokenMaterial>> {\n    use codewhale_config::device_code::DevicePollOutcome;\n    let token_endpoint = oauth_endpoint_url(token_endpoint)?;\n    let client = oauth_http_client(\"device-code poll\")?;\n    let params = [\n        (\"client_id\", client_id),\n        (\"grant_type\", \"urn:ietf:params:oauth:grant-type:device_code\"),\n        (\"device_code\", device_code),\n    ];\n    #[cfg(test)]","sourceCodeStart":736,"sourceCodeEnd":772,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/oauth.rs#L736-L772","documentation":"Thrown when the device-code endpoint responds with HTTP success and no `error` field, but the response body lacks a non-empty `device_code` or `user_code`. This is a response-shape invariant: a 200 without the required grant fields means the provider's device flow is broken or the response was not actually a device grant.","triggerScenarios":"`request_device_grant` receives a 2xx response whose parsed body has empty, whitespace-only, or missing `device_code`/`user_code` fields — e.g. the discovery resolved to the token endpoint instead of the device-authorization endpoint, or the provider returned an empty JSON body.","commonSituations":"Misconfigured provider metadata (wrong `device_code_path` or a discovery document advertising a device endpoint that returns something else); provider API version change altering field names; a proxy stripping or truncating the JSON body.","solutions":["Verify the provider's device-authorization endpoint is correct (check discovery metadata / `device_code_path` config)","Check the provider's API changelog for renamed device-grant response fields","Capture the raw response body (proxy/logging) to confirm what the provider actually returned","Fall back to browser (PKCE) sign-in: `codewhale` login without the device flow"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// validate the grant response before using it\nif (!body.device_code || !body.device_code.trim() || !body.user_code || !body.user_code.trim()) {\n    throw new Error(\"device grant response missing device_code/user_code\");\n}","typeGuard":"function isValidDeviceGrant(b) {\n  return typeof b?.device_code === 'string' && b.device_code.trim() !== '' &&\n         typeof b?.user_code === 'string' && b.user_code.trim() !== '';\n}","tryCatchPattern":null,"preventionTips":["Pin/verify the provider's discovery metadata points at the real device_authorization_endpoint","Log raw response bodies for OAuth endpoints in debug mode","Watch provider API changelogs for device-flow response changes"],"tags":["oauth","api-contract","unexpected-response"],"backgroundTag":"unexpected-api-response-shape","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"}