slopus/happy · error

Token exchange failed: ${tokenResponse.statusText}

Error message

Token exchange failed: ${tokenResponse.statusText}

What it means

Thrown by exchangeCodeForTokens when the OAuth token endpoint responds with a non-OK HTTP status. The CLI sent the authorization code + PKCE verifier to Anthropic's token endpoint, and the server rejected it; only statusText is surfaced, not the response body.

Source

Thrown at packages/happy-cli/src/commands/connect/authenticateClaude.ts:103

): Promise<ClaudeAuthTokens> {

    // Exchange code for tokens
    const tokenResponse = await fetch(TOKEN_URL, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            grant_type: 'authorization_code',
            code: code,
            redirect_uri: `http://localhost:${port}/callback`,
            client_id: CLIENT_ID,
            code_verifier: verifier,
            state: state,
        }),
    });
    if (!tokenResponse.ok) {
        throw new Error(`Token exchange failed: ${tokenResponse.statusText}`);
    }

    // {
    //     token_type: 'Bearer',
    //     access_token: string,
    //     expires_in: number,
    //     refresh_token: string,
    //     scope: 'user:inference',
    //     organization: {
    //       uuid: string,
    //       name: string
    //     },
    //     account: {
    //       uuid: string,
    //       email_address: string
    //     }
    //   }
    const tokenData = await tokenResponse.json() as any;

View on GitHub (pinned to b824cd0a46)

Solutions

  1. Re-run the full `happy connect` / authentication flow to get a fresh authorization code
  2. Check that the redirect callback URL/port matches the redirect_uri sent in the token request
  3. Verify CLIENT_ID and token endpoint URL are current for your CLI version
  4. Capture response body by logging tokenResponse.text() before throwing to see the server's reason

Example fix

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

Strategy: try-catch

Validate before calling

// Re-check prerequisites before calling the connect flow
if (!navigator_online()) throw new Error('Network required for token exchange');
// Ensure a fresh auth code: restart the flow instead of reusing a code

Try / catch

try {
  await authenticateClaude(tokens);
} catch (err) {
  if (String(err.message).startsWith('Token exchange failed')) {
    // restart full auth flow to obtain a fresh authorization code
    await startAuthFlow();
  } else throw err;
}

Prevention

When it happens

Trigger: The POST to the token endpoint returns !response.ok — e.g. expired/already-used authorization code, PKCE code_verifier mismatch, wrong client_id, or server outage.

Common situations: User waits too long between auth-start and callback (code expired), retries the flow reusing a consumed code, clock skew invalidating the verifier, or a proxy/firewall mangling the request.

Related errors


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