{"record":{"id":"8fb86764d8437dc2","repo":"slopus/happy","slug":"token-exchange-failed-8fb867","errorCode":null,"errorMessage":"Token exchange failed","messagePattern":"Token exchange failed","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"packages/happy-cli/src/commands/connect/authenticateGemini.ts","lineNumber":172,"sourceCode":"                    server.close();\n                    reject(new Error('No authorization code received'));\n                    return;\n                }\n                \n                try {\n                    // Exchange code for tokens\n                    const tokens = await exchangeCodeForTokens(code, verifier, port);\n                    \n                    // Redirect to success page\n                    res.writeHead(302, { \n                        'Location': 'https://developers.google.com/gemini-code-assist/auth_success_gemini' \n                    });\n                    res.end();\n                    \n                    server.close();\n                    resolve(tokens);\n                } catch (error) {\n                    res.writeHead(500);\n                    res.end('Token exchange failed');\n                    server.close();\n                    reject(error);\n                }\n            }\n        });\n        \n        server.listen(port, '127.0.0.1', () => {\n            // console.log(`🔐 OAuth callback server listening on port ${port}`);\n        });\n        \n        // Timeout after 5 minutes\n        setTimeout(() => {\n            server.close();\n            reject(new Error('Authentication timeout'));\n        }, 5 * 60 * 1000);\n    });\n}","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/slopus/happy/blob/b824cd0a4681d41af631a8e422a813873e4455b0/packages/happy-cli/src/commands/connect/authenticateGemini.ts#L154-L190","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Rerun the connect flow for a fresh code and verifier; do not retry with the same code or reload the callback page.","Check the logged token-endpoint error body for the exact Google error code (invalid_grant, invalid_client, redirect_uri_mismatch).","Verify network access to oauth2.googleapis.com (VPN/proxy/firewall) and correct system time.","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."],"exampleFix":"// before: reloading the callback page replays a spent code → 500 Token exchange failed\n// after: always start a new flow on failure\ntry {\n  const tokens = await authenticateGemini();\n} catch (e) {\n  // rerun authenticateGemini() — fresh code, verifier, and state\n}","handlingStrategy":"retry","validationCode":"// Confirm Google's token endpoint is reachable before the OAuth flow\nconst reachable = await fetch('https://oauth2.googleapis.com').then(r => r.status < 500).catch(() => false);\nif (!reachable) throw new Error('oauth2.googleapis.com unreachable — check network/proxy before authenticating');","typeGuard":null,"tryCatchPattern":"async function connectGeminiWithRetry(maxAttempts = 2): Promise<GeminiAuthTokens> {\n  let lastErr: unknown;\n  for (let i = 0; i < maxAttempts; i++) {\n    try {\n      return await authenticateGemini();\n    } catch (err) {\n      lastErr = err;\n      const msg = err instanceof Error ? err.message : String(err);\n      if (/Token exchange failed|invalid_grant/i.test(msg)) continue; // restart flow with a fresh code\n      throw err;\n    }\n  }\n  throw lastErr;\n}","preventionTips":["Do not refresh the callback page; a replayed code fails the token exchange with invalid_grant.","Check connectivity to oauth2.googleapis.com and keep system time NTP-synchronized.","Avoid mid-flow port changes — keep the default callback port free so redirect_uri matches between authorize and token calls.","If errors persist, read the logged Google error body (invalid_client, redirect_uri_mismatch) and update happy-cli."],"tags":["oauth","token-exchange","google","network","pkce"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"b824cd0a4681d41af631a8e422a813873e4455b0","analyzedAt":"2026-08-31T23:12:36.205Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}