{"record":{"id":"6c612668b0793132","repo":"decolua/9router","slug":"token-exchange-failed-error","errorCode":null,"errorMessage":"`Token exchange failed: ${error}`","messagePattern":"`Token exchange failed: (.+?)`","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/providers/antigravity.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    const loadHeaders = {\n      \"Authorization\": `Bearer ${tokens.access_token}`,\n      \"Content-Type\": \"application/json\",\n      \"User-Agent\": ANTIGRAVITY_CONFIG.loadCodeAssistUserAgent,\n      \"x-request-source\": \"local\",\n    };\n    const metadata = getOAuthClientMetadata();\n\n    // Fetch user info\n    const userInfoRes = await fetch(`${ANTIGRAVITY_CONFIG.userInfoUrl}?alt=json`, {\n      headers: {\n        Authorization: `Bearer ${tokens.access_token}`,\n        \"x-request-source\": \"local\",","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/providers/antigravity.js#L18-L54","documentation":"antigravity.js exchangeToken POSTs the authorization code to the provider token endpoint and, when the HTTP response status is not ok, throws this error with the raw response body appended. It means the upstream OAuth server refused the authorization-code exchange (invalid code, redirect_uri mismatch, expired code, bad client credentials, etc.).","triggerScenarios":"Calling exchangeToken after the browser callback with: an authorization code already consumed or expired, a redirect_uri different from the one used in buildAuthUrl, wrong/missing client_secret in ANTIGRAVITY_CONFIG, or a 4xx/5xx from the token endpoint (rate limit, upstream outage).","commonSituations":"User refreshing the callback page (code replay), starting the OAuth flow behind a different port/redirect than configured, stale deployed config after provider rotated its client secret, or network middleboxes returning HTML error pages.","solutions":["Read the appended response body — it contains the OAuth error (e.g. invalid_grant, redirect_uri_mismatch) and fix that specific cause.","invalid_grant: restart the whole OAuth flow; authorization codes are single-use and expire in minutes.","redirect_uri_mismatch: ensure the same redirectUri is passed to buildAuthUrl and exchangeToken.","invalid_client: verify config.clientId/clientSecret against current provider settings.","Check upstream status page / retry if the body indicates a 5xx outage."],"exampleFix":"// before: mismatched redirect_uri between steps\nbuildAuthUrl(config, 'http://localhost:20128/callback', state)\nexchangeToken(config, code, 'http://localhost:3000/callback') // 400 redirect_uri_mismatch\n// after: reuse the identical redirect URI\nconst redirectUri = 'http://localhost:20128/callback';\nbuildAuthUrl(config, redirectUri, state);\nawait exchangeToken(config, code, redirectUri);","handlingStrategy":"try-catch","validationCode":"const code = new URL(callbackUrl).searchParams.get('code');\nif (!code) throw new Error('callback URL missing code param — abort before exchanging');\nif (exchangedCodes.has(code)) throw new Error('authorization code already consumed');","typeGuard":"function hasValidCodeParams(params) {\n  return typeof params?.code === 'string' && params.code.length > 0 && typeof params?.redirectUri === 'string' && params.redirectUri.length > 0;\n}","tryCatchPattern":"try {\n  const tokens = await antigravity.exchangeToken(config, code, redirectUri);\n  // use tokens\n} catch (err) {\n  if (String(err.message).startsWith('Token exchange failed:')) {\n    const body = err.message.slice('Token exchange failed:'.length);\n    if (/invalid_grant/.test(body)) startNewAuthFlow();          // code expired/used\n    else if (/redirect_uri_mismatch/.test(body)) fixRedirectUri();\n    else if (/invalid_client/.test(body)) checkClientSecret();\n    else retryWithBackoff();                                     // 5xx\n  } else throw err;\n}","preventionTips":["Use a single shared redirectUri constant for both buildAuthUrl and exchangeToken.","Mark authorization codes as consumed to block callback-page refreshes from replaying them.","Exchange codes immediately after the callback; they expire within minutes.","Parse the appended response body (it contains the OAuth error code) before deciding to retry.","Keep ANTIGRAVITY_CONFIG client credentials in sync with provider settings."],"tags":["oauth","token-exchange","antigravity","http-4xx"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}