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

  1. Log/inspect the full response body (add a debug print of `body` before the check) to see what the endpoint actually returned
  2. Verify the token endpoint URL (CODEX_TOKEN_URL) is not overridden to a wrong/stale value via env or config
  3. Retry the login flow — a transient provider-side issue returning an empty/error 200 body usually resolves
  4. If behind a proxy, captive portal, or corporate TLS-inspection appliance, bypass it for the auth host so the real JSON token response arrives
  5. 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

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


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)