{"record":{"id":"71d924e1ac22b717","repo":"Kuberwastaken/claurst","slug":"no-access-token-in-response","errorCode":null,"errorMessage":"No access_token in response","messagePattern":"No access_token in response","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-rust/crates/cli/src/codex_oauth_flow.rs","lineNumber":219,"sourceCode":"\r\n    if !resp.status().is_success() {\r\n        let status = resp.status();\r\n        let body = resp.text().await.unwrap_or_default();\r\n        bail!(\"Token exchange failed ({}): {}\", status, body);\r\n    }\r\n\r\n    let body: serde_json::Value = resp\r\n        .json()\r\n        .await\r\n        .map_err(|e| anyhow!(\"Failed to parse token response: {}\", e))?;\r\n\r\n    let access_token = body[\"access_token\"]\r\n        .as_str()\r\n        .unwrap_or(\"\")\r\n        .to_string();\r\n\r\n    if access_token.is_empty() {\r\n        bail!(\"No access_token in response\");\r\n    }\r\n\r\n    let refresh_token = body[\"refresh_token\"].as_str().map(|s| s.to_string());\r\n    let account_id = extract_account_id_from_jwt(&access_token);\r\n\r\n    Ok(CodexTokens {\r\n        access_token,\r\n        refresh_token,\r\n        account_id,\r\n        expires_at: None,\r\n    })\r\n}\r\n\r\n/// Extract chatgpt-account-id from the JWT access token.\r\n/// The account_id is in the middle segment (payload) under\r\n/// https://api.openai.com/auth.account_id\r\nfn extract_account_id_from_jwt(token: &str) -> Option<String> {\r\n    let parts: Vec<&str> = token.splitn(3, '.').collect();\r","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/Kuberwastaken/claurst/blob/b0637c97ec34144387cbf2f74f65df6d16a6cef1/src-rust/crates/cli/src/codex_oauth_flow.rs#L201-L237","documentation":"The token endpoint returned HTTP 2xx and the body parsed as JSON, but the parsed object contains no non-empty `access_token` string. exchange_code_for_tokens reads `body[\"access_token\"]` and bails when it is empty, because every downstream operation (API calls, JWT account-id extraction) depends on it.","triggerScenarios":"The token endpoint responds 200 with a JSON body that either is not the expected token object (e.g. an error envelope with 200 status, a different schema, or an HTML/XML page misparsed), or includes `access_token` as null/non-string, or omits the field entirely.","commonSituations":"Auth provider API contract change or partial outage returning 200 with an error body; a captive portal or proxy returning a 200 HTML page that serde_json parses into something without the field (or where the earlier resp.json() parse would fail); misconfigured provider environment (e.g. a test/mock token endpoint with a different response shape); typo'd base URL pointing at the wrong service.","solutions":["Log/inspect the full response body (add a debug print of `body` before the check) to see what the endpoint actually returned","Verify the token endpoint URL (CODEX_TOKEN_URL) is not overridden to a wrong/stale value via env or config","Retry the login flow — a transient provider-side issue returning an empty/error 200 body usually resolves","If behind a proxy, captive portal, or corporate TLS-inspection appliance, bypass it for the auth host so the real JSON token response arrives","Update claurst / check provider status page in case the token response schema changed"],"exampleFix":"// before: 200 response with an unexpected envelope\n// {\"error\": null, \"tokens\": null}\nlet access_token = body[\"access_token\"].as_str().unwrap_or(\"\").to_string();\n\n// after: surface the unexpected body instead of a bare bail\nlet access_token = body[\"access_token\"].as_str().filter(|s| !s.is_empty())\n    .ok_or_else(|| anyhow!(\"No access_token in response: {}\", body))?;","handlingStrategy":"validation","validationCode":"// After parsing, verify the token object shape before consuming it\nfn has_access_token(body: &serde_json::Value) -> bool {\n    body[\"access_token\"].as_str().map(|s| !s.is_empty()).unwrap_or(false)\n}","typeGuard":"fn is_token_response(v: &serde_json::Value) -> bool {\n    v.is_object() && v[\"access_token\"].is_string() && !v[\"access_token\"].as_str().unwrap().is_empty()\n}","tryCatchPattern":"let body: serde_json::Value = resp.json().await?;\nif !is_token_response(&body) {\n    anyhow::bail!(\"unexpected token response: {}\", body);\n}","preventionTips":["Log the raw token-endpoint response (redacted) when auth fails to catch schema/proxy surprises","Pin/verify the token endpoint URL; beware env overrides pointing at mocks","Watch for 200-with-error-body responses from the auth provider during incidents"],"tags":["oauth","json","unexpected-response","token-exchange"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"b0637c97ec34144387cbf2f74f65df6d16a6cef1","analyzedAt":"2026-09-10T00:24:58.650Z","contentChangedAt":"2026-09-10T00:24:58.650Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}