{"record":{"id":"a685b605d2f047b9","repo":"decolua/9router","slug":"token-exchange-failed-error-a685b6","errorCode":null,"errorMessage":"Token exchange failed: ${error}","messagePattern":"Token exchange failed: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/services/antigravity.js","lineNumber":55,"sourceCode":"  async exchangeCode(code, redirectUri) {\n    const response = await fetch(this.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: this.config.clientId,\n        client_secret: this.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\n  /**\n   * Get user info from Google\n   */\n  async getUserInfo(accessToken) {\n    const response = await fetch(`${this.config.userInfoUrl}?alt=json`, {\n      headers: {\n        Authorization: `Bearer ${accessToken}`,\n        Accept: \"application/json\",\n      },\n    });\n\n    if (!response.ok) {\n      const error = await response.text();","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/services/antigravity.js#L37-L73","documentation":"AntigravityService.exchangeCode POSTs grant_type=authorization_code (with client_secret) to the configured tokenUrl and throws this on any non-2xx response, embedding the raw body. It is the standard OAuth2 token-endpoint failure: the authorization code could not be exchanged for tokens.","triggerScenarios":"Code expired/already used (invalid_grant); client_id/client_secret wrong or rotated; redirect_uri mismatch vs the authorize request; Google token endpoint returning 400/401/5xx; clock skew affecting code validity.","commonSituations":"Retrying after a failed first exchange (code consumed); ANTIGRAVITY_CONFIG credentials outdated; callback served on a different port/path than registered; Google-side outage.","solutions":["Read the embedded OAuth error body (invalid_grant, invalid_client, redirect_uri_mismatch) and act on it","Restart the full OAuth flow with a fresh code — never retry a used code","Verify client_id/client_secret and redirect_uri in ANTIGRAVITY_CONFIG exactly match the registered OAuth client","Confirm the callback redirect_uri string is byte-identical to the one in buildAuthUrl","If 5xx, wait and retry with a brand-new authorization code"],"exampleFix":"// before\nconst tokens = await svc.exchangeCode(code, redirectUri);\n// after\nlet tokens;\ntry { tokens = await svc.exchangeCode(code, redirectUri); }\ncatch (e) {\n  if (/invalid_grant/.test(e.message)) throw new Error('Code expired or already used — reconnect to restart the flow');\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"// pre-flight before exchanging\nif (!code) throw new Error('No authorization code — restart OAuth');\nif (!ANTIGRAVITY_CONFIG.clientId || !ANTIGRAVITY_CONFIG.clientSecret) throw new Error('Antigravity OAuth client credentials missing');\nif (authRedirectUri !== redirectUri) throw new Error('redirect_uri differs from the authorize request');","typeGuard":"const isExchangeError = (e) => e instanceof Error && e.message.startsWith('Token exchange failed:');\nconst oauthErrorCode = (e) => { const m = e.message.match(/\"error\"\\s*:\\s*\"([^\"]+)\"/); return m ? m[1] : null; };","tryCatchPattern":"try { tokens = await svc.exchangeCode(code, redirectUri); }\ncatch (e) {\n  if (!isExchangeError(e)) throw e;\n  const codeErr = oauthErrorCode(e);           // invalid_grant | invalid_client | redirect_uri_mismatch\n  if (codeErr === 'invalid_grant') startNewOAuthFlow();\n  else throw new Error(`Antigravity client config problem (${codeErr}) — check clientId/secret/redirect_uri`);\n}","preventionTips":["Treat codes as single-use: one exchange attempt, then a fresh flow on failure","Keep client_secret/client_id current — Google clients get rotated","Match redirect_uri exactly (scheme, port, path) between authorize and token calls","Watch for Google-side endpoint changes in ANTIGRAVITY_CONFIG.tokenUrl"],"tags":["oauth","token-exchange","antigravity","google"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}