Kuberwastaken/claurst · error · anyhow::Error
No access_token in response
Error message
No access_token in response
What it means
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.
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
Example fix
// before: 200 response with an unexpected envelope
// {"error": null, "tokens": null}
let access_token = body["access_token"].as_str().unwrap_or("").to_string();
// after: surface the unexpected body instead of a bare bail
let access_token = body["access_token"].as_str().filter(|s| !s.is_empty())
.ok_or_else(|| anyhow!("No access_token in response: {}", body))?; Defensive patterns
Strategy: validation
Validate before calling
// After parsing, verify the token object shape before consuming it
fn has_access_token(body: &serde_json::Value) -> bool {
body["access_token"].as_str().map(|s| !s.is_empty()).unwrap_or(false)
} Type guard
fn is_token_response(v: &serde_json::Value) -> bool {
v.is_object() && v["access_token"].is_string() && !v["access_token"].as_str().unwrap().is_empty()
} Try / catch
let body: serde_json::Value = resp.json().await?;
if !is_token_response(&body) {
anyhow::bail!("unexpected token response: {}", body);
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.
Related errors
- Failed to parse token response
- Bridge poll: auth error
- Token exchange failed
- Token exchange failed
- exchange_code: HTTP
AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10).
Data as JSON: /api/errors/71d924e1ac22b717.
Report an issue: GitHub.
Appendix: source
Thrown at src-rust/crates/cli/src/codex_oauth_flow.rs:219
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
bail!("Token exchange failed ({}): {}", status, body);
}
let body: serde_json::Value = resp
.json()
.await
.map_err(|e| anyhow!("Failed to parse token response: {}", e))?;
let access_token = body["access_token"]
.as_str()
.unwrap_or("")
.to_string();
if access_token.is_empty() {
bail!("No access_token in response");
}
let refresh_token = body["refresh_token"].as_str().map(|s| s.to_string());
let account_id = extract_account_id_from_jwt(&access_token);
Ok(CodexTokens {
access_token,
refresh_token,
account_id,
expires_at: None,
})
}
/// Extract chatgpt-account-id from the JWT access token.
/// The account_id is in the middle segment (payload) under
/// https://api.openai.com/auth.account_id
fn extract_account_id_from_jwt(token: &str) -> Option<String> {
let parts: Vec<&str> = token.splitn(3, '.').collect();
View on GitHub (pinned to b0637c97ec)