{"record":{"id":"885084b64ec86d0b","repo":"decolua/9router","slug":"token-exchange-failed-error-885084","errorCode":null,"errorMessage":"`Token exchange failed: ${error}`","messagePattern":"`Token exchange failed: (.+?)`","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/providers/gemini-cli.js","lineNumber":36,"sourceCode":"  exchangeToken: async (config, code, redirectUri) => {\n    const response = await fetch(config.tokenUrl, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        Accept: \"application/json\",\n      },\n      body: new URLSearchParams({\n        grant_type: \"authorization_code\",\n        client_id: config.clientId,\n        client_secret: config.clientSecret,\n        code: code,\n        redirect_uri: redirectUri,\n      }),\n    });\n\n    if (!response.ok) {\n      const error = await response.text();\n      throw new Error(`Token exchange failed: ${error}`);\n    }\n\n    return await response.json();\n  },\n  postExchange: async (tokens) => {\n    // Fetch user info\n    const userInfoRes = await fetch(`${GEMINI_CONFIG.userInfoUrl}?alt=json`, {\n      headers: { Authorization: `Bearer ${tokens.access_token}` },\n    });\n    const userInfo = userInfoRes.ok ? await userInfoRes.json() : {};\n\n    // Fetch project ID\n    let projectId = \"\";\n    try {\n      const projectRes = await fetch(\n        \"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist\",\n        {\n          method: \"POST\",","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/providers/gemini-cli.js#L18-L54","documentation":"Gemini CLI OAuth token exchange: the POST trading the authorization code (plus redirect_uri) for tokens returned a non-2xx status. The body text is interpolated into this Error. Because mapTokens/postExchange never run, no Gemini credentials are stored and the login fails.","triggerScenarios":"Calling exchangeCode for gemini-cli when Google's OAuth token endpoint responds !response.ok — invalid_grant (code expired, already used, or clock skew), invalid_client, redirect_uri mismatch, or transient 5xx/429.","commonSituations":"Machine clock skewed (Google rejects grants); user delayed past the code's ~10-minute life; redirect_uri differs between authorize and exchange (e.g. different port on localhost); stale/revoked OAuth client; Google outage.","solutions":["Read the body's 'error' field to distinguish invalid_grant vs invalid_client vs server errors.","Sync system clock (NTP) and restart the login flow to get a fresh code.","Ensure the redirect_uri in the exchange matches the authorize request exactly.","Check Google OAuth client config / Gemini CLI version for changed client credentials."],"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  let code = \"unknown\";\n  try { code = JSON.parse(error).error; } catch {}\n  throw new Error(`Token exchange failed (HTTP ${response.status}, ${code}): ${error}`);\n}","handlingStrategy":"try-catch","validationCode":"// Pre-exchange checks for Gemini login\nif (!code) throw new Error(\"No authorization code for Gemini exchange\");\nif (Math.abs(Date.now() - (await ntpOffset())) > 90_000) throw new Error(\"Clock skew too large for Google OAuth — sync NTP\");","typeGuard":"function isGoogleOAuthError(body) {\n  return typeof body === \"object\" && body !== null &&\n    typeof body.error === \"string\"; // e.g. \"invalid_grant\"\n}","tryCatchPattern":"try {\n  const tokens = await geminiProvider.exchangeCode(code, redirectUri);\n} catch (err) {\n  if (String(err.message).includes(\"Token exchange failed\")) {\n    if (/invalid_grant/.test(err.message)) {\n      console.error(\"Gemini code expired/replayed or clock skew — restart login\");\n      await restartGeminiLogin();\n    } else {\n      console.error(\"Gemini token endpoint error:\", err.message);\n    }\n  } else throw err;\n}","preventionTips":["Keep the system clock NTP-synced; Google rejects grants under skew.","Use an identical redirect_uri string for authorize and exchange (watch localhost ports).","Add a timeout so a hung Google token call fails before the code expires.","Distinguish invalid_grant (restart flow) from server errors (safe retry)."],"tags":["oauth","google","token-exchange","http-error"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}