{"record":{"id":"96619c416515b5ac","repo":"slopus/happy","slug":"token-exchange-failed-error-96619c","errorCode":null,"errorMessage":"Token exchange failed: ${error}","messagePattern":"Token exchange failed: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/happy-cli/src/commands/connect/authenticateGemini.ts","lineNumber":109,"sourceCode":"): Promise<GeminiAuthTokens> {\n    const response = await fetch(TOKEN_URL, {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/x-www-form-urlencoded',\n        },\n        body: new URLSearchParams({\n            grant_type: 'authorization_code',\n            client_id: CLIENT_ID,\n            client_secret: CLIENT_SECRET,\n            code: code,\n            code_verifier: verifier,\n            redirect_uri: `http://localhost:${port}/oauth2callback`,\n        }),\n    });\n    \n    if (!response.ok) {\n        const error = await response.text();\n        throw new Error(`Token exchange failed: ${error}`);\n    }\n    \n    const data = await response.json() as GeminiAuthTokens;\n    return data;\n}\n\n/**\n * Start local server to handle OAuth callback\n */\nasync function startCallbackServer(\n    state: string,\n    verifier: string,\n    port: number\n): Promise<GeminiAuthTokens> {\n    return new Promise((resolve, reject) => {\n        const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n            const url = new URL(req.url!, `http://localhost:${port}`);\n            ","sourceCodeStart":91,"sourceCodeEnd":127,"githubUrl":"https://github.com/slopus/happy/blob/b824cd0a4681d41af631a8e422a813873e4455b0/packages/happy-cli/src/commands/connect/authenticateGemini.ts#L91-L127","documentation":"Thrown by exchangeCodeForTokens for the Gemini (Google) OAuth flow when the token endpoint responds non-OK. The raw response body is included in the message, so the Google error JSON (e.g. invalid_grant) is visible.","triggerScenarios":"POST to Google's oauth2 token endpoint with the authorization code and redirect_uri http://localhost:<port>/oauth2callback returns !response.ok — expired/used code, code_verifier mismatch, revoked consent, or quota/5xx.","commonSituations":"User took too long to approve consent (code expired), re-ran flow reusing a code, redirect port mismatch, or Google rejecting due to changed OAuth client settings.","solutions":["Parse the JSON error body in the message (e.g. invalid_grant means the code expired) and restart the auth flow","Confirm the redirect_uri port matches the local callback server","Re-run `happy connect` Gemini authentication end-to-end","Verify OAuth client credentials are still valid in Google Cloud Console"],"exampleFix":"// before\nif (!response.ok) {\n    const error = await response.text();\n    throw new Error(`Token exchange failed: ${error}`);\n}\n// after\nif (!response.ok) {\n    const error = await response.text();\n    throw new Error(`Token exchange failed (HTTP ${response.status}): ${error}`);\n}","handlingStrategy":"try-catch","validationCode":"// Ensure a code exists and callback port matches redirect_uri\nif (!authCode) throw new Error('No authorization code captured');\nif (!redirectUri.includes('/oauth2callback')) throw new Error('Wrong Gemini redirect path');","typeGuard":null,"tryCatchPattern":"try {\n  const tokens = await exchangeCodeForTokens(code, port);\n} catch (err) {\n  if (String(err.message).startsWith('Token exchange failed')) {\n    // body usually contains Google error like invalid_grant\n    console.error('Gemini auth error:', err.message);\n    await runGeminiAuth(); // fresh code\n  } else throw err;\n}","preventionTips":["Approve the consent screen quickly to avoid code expiry","Re-run the full flow instead of retrying a used code","Verify OAuth client credentials in Google Cloud Console","Confirm the /oauth2callback port is free and consistent"],"tags":["oauth","network","authentication"],"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"}