slopus/happy · error

Token exchange failed: ${error}

Error message

Token exchange failed: ${error}

What it means

Thrown by exchangeCodeForTokens for the Codex (OpenAI) OAuth flow when the token endpoint responds non-OK. Unlike the Claude variant, it includes the response body text in the message, so the server's error description is visible.

Source

Thrown at packages/happy-cli/src/commands/connect/authenticateCodex.ts:111

    port: number
): Promise<CodexAuthTokens> {
    const response = await fetch(`${AUTH_BASE_URL}/oauth/token`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: new URLSearchParams({
            grant_type: 'authorization_code',
            client_id: CLIENT_ID,
            code: code,
            code_verifier: verifier,
            redirect_uri: `http://localhost:${port}/auth/callback`,
        }),
    });

    if (!response.ok) {
        const error = await response.text();
        throw new Error(`Token exchange failed: ${error}`);
    }

    const data = (await response.json() as any);

    // Parse ID token to get account ID
    const idTokenPayload = parseJWT(data.id_token);

    // The account ID is stored at chatgpt_account_id in the payload
    let accountId = idTokenPayload.chatgpt_account_id;

    // Check nested location
    if (!accountId) {
        const authClaim = idTokenPayload['https://api.openai.com/auth'];
        if (authClaim && typeof authClaim === 'object') {
            accountId = authClaim.chatgpt_account_id || authClaim.account_id;
        }
    }

View on GitHub (pinned to b824cd0a46)

Solutions

  1. Read the error body in the message and follow the provider's documented cause (e.g. invalid_grant)
  2. Re-run the Codex authentication flow for a fresh authorization code
  3. Ensure the callback port matches the redirect_uri exactly
  4. Update the CLI if the provider changed token-endpoint requirements

Example fix

// before
if (!response.ok) {
    const error = await response.text();
    throw new Error(`Token exchange failed: ${error}`);
}
// after
if (!response.ok) {
    const error = await response.text();
    throw new Error(`Token exchange failed (HTTP ${response.status}): ${error}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Confirm the callback port matches the redirect_uri before exchanging
if (!redirectUri.startsWith(`http://localhost:${port}/auth/callback`)) {
  throw new Error('redirect_uri/port mismatch');
}

Try / catch

try {
  await exchangeCodeForTokens(code, port);
} catch (err) {
  if (String(err.message).startsWith('Token exchange failed')) {
    console.error('Codex token exchange rejected:', err.message);
    // restart auth to get a fresh code
    await runCodexAuth();
  } else throw err;
}

Prevention

When it happens

Trigger: POST to the OpenAI token endpoint with authorization_code, code_verifier, and redirect_uri http://localhost:<port>/auth/callback returns !response.ok — expired/replayed code, PKCE mismatch, client misconfiguration, or 5xx.

Common situations: Callback server bound to a different port than redirect_uri, user retried an already-consumed code, corporate proxy intercepting localhost callback or outbound call, provider outage.

Related errors


AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31). Data as JSON: /api/errors/69555ae46c27bf19. Report an issue: GitHub.