{"record":{"id":"a4f29941ace89daa","repo":"Hmbown/CodeWhale","slug":"name-oauth-operation-returned-an-empty-access-token","errorCode":null,"errorMessage":"{name} OAuth {operation} returned an empty access token","messagePattern":"(.+?) OAuth (.+?) returned an empty access token","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/oauth.rs","lineNumber":993,"sourceCode":"    })?;\n    if !(200..300).contains(&status) || parsed.error.is_some() {\n        let err = parsed.error.as_deref().unwrap_or(\"token_error\");\n        if matches!(\n            err,\n            \"invalid_grant\"\n                | \"refresh_token_reused\"\n                | \"refresh_token_expired\"\n                | \"refresh_token_invalidated\"\n        ) || status == 401\n        {\n            bail!(\n                \"{name} OAuth {operation} failed permanently ({err}). Sign in again with `{}`.\",\n                params.relogin_hint\n            );\n        }\n        bail!(\"{name} OAuth {operation} failed ({err})\");\n    }\n    anyhow::ensure!(\n        parsed\n            .access_token\n            .as_deref()\n            .is_some_and(|token| !token.trim().is_empty()),\n        \"{name} OAuth {operation} returned an empty access token\"\n    );\n    Ok(parsed)\n}\n\nfn compact_form_error(body: &str) -> String {\n    body.chars().filter(|c| !c.is_control()).take(80).collect()\n}\n\n/// Refresh an owned token through the seam at an explicit token URL —\n/// discovered when the provider row demands it, pinned otherwise. Refresh is\n/// a Codewhale-owned credential operation only: external imports never\n/// refresh.\npub(crate) fn refresh_access_token_via(","sourceCodeStart":975,"sourceCodeEnd":1011,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/oauth.rs#L975-L1011","documentation":"After the token response parses successfully, the library requires a non-empty `access_token`. If the server returned well-formed token JSON whose `access_token` is missing, null, or whitespace-only, this ensure! fires with the provider name and operation. This guards against providers that report success semantics while omitting the credential the library actually needs.","triggerScenarios":"A token exchange/refresh/device-poll response deserializes as `OAuthTokenMaterial` but has `access_token: null` or `access_token: \"\"` (or only whitespace).","commonSituations":"A non-standard OAuth provider returning `{ \"error\": \"...\" }`-adjacent shapes without the error field populated; a partially implemented/idiosyncratic token endpoint; a mock or stub server used in development that returns empty token JSON.","solutions":["Fix the provider/token endpoint so it returns a real non-empty access_token","Check whether the server returned a token error without an `error` field — inspect the raw response with curl","If testing against a stub, populate `access_token` in the fixture"],"exampleFix":"// before: idiosyncratic response missing the token\n{\"token_type\": \"bearer\", \"expires_in\": 3600}\n// after: standard token response\n{\"access_token\": \"eyJ...\", \"token_type\": \"bearer\", \"expires_in\": 3600}","handlingStrategy":"validation","validationCode":"fn token_payload_has_access_token(v: &serde_json::Value) -> bool {\n    v.get(\"access_token\")\n        .and_then(|t| t.as_str())\n        .map_or(false, |s| !s.trim().is_empty())\n}","typeGuard":"fn has_nonempty_access_token(m: &OAuthTokenMaterial) -> bool {\n    m.access_token.as_deref().is_some_and(|t| !t.trim().is_empty())\n}","tryCatchPattern":"match poll_device_grant(...) {\n    Ok(m) if has_nonempty_access_token(&m) => m,\n    Ok(_) => anyhow::bail!(\"provider returned empty access token\"),\n    Err(e) => return Err(e),\n}","preventionTips":["Verify the provider conforms to RFC 6749 token responses before integration","Capture and inspect a raw token response during provider onboarding","If using a stub/mock server, always populate access_token in fixtures"],"tags":["oauth","access-token","validation"],"backgroundTag":"unexpected-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"}