slopus/happy · error

Token exchange failed

Error message

Token exchange failed

What it means

In the Gemini flow, once `code` and `state` validate, exchangeCodeForTokens() POSTs the code plus PKCE verifier, client_id, and client_secret to Google's token endpoint. Any throw inside this try block — a non-2xx token response ('Token exchange failed: <body>'), a network failure, or a JSON parse error — is caught, the browser receives HTTP 500 'Token exchange failed', and the promise rejects with the original error.

Source

Thrown at packages/happy-cli/src/commands/connect/authenticateGemini.ts:172

                    server.close();
                    reject(new Error('No authorization code received'));
                    return;
                }
                
                try {
                    // Exchange code for tokens
                    const tokens = await exchangeCodeForTokens(code, verifier, port);
                    
                    // Redirect to success page
                    res.writeHead(302, { 
                        'Location': 'https://developers.google.com/gemini-code-assist/auth_success_gemini' 
                    });
                    res.end();
                    
                    server.close();
                    resolve(tokens);
                } catch (error) {
                    res.writeHead(500);
                    res.end('Token exchange failed');
                    server.close();
                    reject(error);
                }
            }
        });
        
        server.listen(port, '127.0.0.1', () => {
            // console.log(`🔐 OAuth callback server listening on port ${port}`);
        });
        
        // Timeout after 5 minutes
        setTimeout(() => {
            server.close();
            reject(new Error('Authentication timeout'));
        }, 5 * 60 * 1000);
    });
}

View on GitHub (pinned to b824cd0a46)

Solutions

  1. Rerun the connect flow for a fresh code and verifier; do not retry with the same code or reload the callback page.
  2. Check the logged token-endpoint error body for the exact Google error code (invalid_grant, invalid_client, redirect_uri_mismatch).
  3. Verify network access to oauth2.googleapis.com (VPN/proxy/firewall) and correct system time.
  4. Ensure the callback port is stable between the authorize and token requests (avoid other processes claiming the port mid-flow); update happy-cli if issues persist.

Example fix

// before: reloading the callback page replays a spent code → 500 Token exchange failed
// after: always start a new flow on failure
try {
  const tokens = await authenticateGemini();
} catch (e) {
  // rerun authenticateGemini() — fresh code, verifier, and state
}
Defensive patterns

Strategy: retry

Validate before calling

// Confirm Google's token endpoint is reachable before the OAuth flow
const reachable = await fetch('https://oauth2.googleapis.com').then(r => r.status < 500).catch(() => false);
if (!reachable) throw new Error('oauth2.googleapis.com unreachable — check network/proxy before authenticating');

Try / catch

async function connectGeminiWithRetry(maxAttempts = 2): Promise<GeminiAuthTokens> {
  let lastErr: unknown;
  for (let i = 0; i < maxAttempts; i++) {
    try {
      return await authenticateGemini();
    } catch (err) {
      lastErr = err;
      const msg = err instanceof Error ? err.message : String(err);
      if (/Token exchange failed|invalid_grant/i.test(msg)) continue; // restart flow with a fresh code
      throw err;
    }
  }
  throw lastErr;
}

Prevention

When it happens

Trigger: The POST to https://oauth2.googleapis.com/token fails: invalid_grant (code expired, already used, or redirect_uri differs from the authorize request), PKCE verifier mismatch, invalid_client (bad/revoked client secret), network/proxy failure, or malformed JSON response.

Common situations: Browser refresh re-sends the callback and replays a consumed code; clock skew invalidating the code within its ~10-minute lifetime; corporate proxy blocking oauth2.googleapis.com; Google-side 5xx outage; redirect_uri port drift between authorize and token calls when the default port was busy.

Related errors


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