{"record":{"id":"ae13ed451b8ca318","repo":"decolua/9router","slug":"xai-token-exchange-failed-error","errorCode":null,"errorMessage":"xAI token exchange failed: ${error}","messagePattern":"xAI token exchange failed: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/providers/xai.js","lineNumber":76,"sourceCode":"  },\n  exchangeToken: async (config, code, redirectUri, codeVerifier) => {\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        code,\n        redirect_uri: redirectUri,\n        code_verifier: codeVerifier,\n      }),\n    });\n    if (!response.ok) {\n      const error = await response.text();\n      throw new Error(`xAI token exchange failed: ${error}`);\n    }\n    return await response.json();\n  },\n  mapTokens: (tokens) => {\n    const mapped = {\n      accessToken: tokens.access_token,\n      refreshToken: tokens.refresh_token,\n      expiresIn: tokens.expires_in,\n      scope: tokens.scope,\n    };\n    const email = decodeXaiIdTokenEmail(tokens.id_token);\n    if (email) mapped.email = email;\n    if (tokens.id_token) {\n      mapped.providerSpecificData = { idToken: tokens.id_token };\n    }\n    return mapped;\n  },\n};","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/providers/xai.js#L58-L94","documentation":"xAI's exchangeToken POSTs the authorization code to the discovered/static token endpoint with PKCE and throws this when the response is non-2xx, embedding the raw error body. Typical token-endpoint rejections are invalid_grant (code expired/used), invalid_client (bad client_id), or code_verifier/code_challenge mismatches.","triggerScenarios":"Authorization code already exchanged or expired (single-use, short TTL); code_verifier does not match the code_challenge sent at authorize time; redirect_uri differs from the one used in buildAuthUrl; discovery fallback switched to a stale token URL; xAI endpoint returns 4xx/5xx.","commonSituations":"Retrying an exchange after a timeout (code already consumed); CLI restart losing the PKCE verifier between authorize and callback; redirect port busy so the callback URI changed; cached endpoint discovery pointing to a rotated token URL.","solutions":["Read the embedded body — invalid_grant means restart the whole flow with a fresh authorize URL","Never retry the same code: authorization codes are single-use; start a new OAuth round on failure","Ensure the exact same redirect_uri is used in buildAuthUrl and exchangeToken","Keep the PKCE code_verifier from flow start through to the exchange (no restarts in between)","Clear cached endpoint discovery / restart so the current token_url is re-fetched"],"exampleFix":"// before: blind retry reuses a consumed code\nlet tokens;\ntry { tokens = await exchangeToken(cfg, code, redirectUri, verifier); }\ncatch { tokens = await exchangeToken(cfg, code, redirectUri, verifier); } // fails again: invalid_grant\n// after: codes are single-use — restart the flow\ntry { tokens = await exchangeToken(cfg, code, redirectUri, verifier); }\ncatch (e) {\n  if (e.message.includes('xAI token exchange failed')) startNewOAuthFlow(); // fresh code + verifier\n  else throw e;\n}","handlingStrategy":"try-catch","validationCode":"// pre-flight: same redirect_uri, non-empty code & verifier\nif (!code || !codeVerifier) throw new Error('Missing code or PKCE verifier — restart the xAI OAuth flow');\nif (redirectUriUsedInAuthUrl !== redirectUri) throw new Error('redirect_uri mismatch between authorize and token calls');","typeGuard":"const isTokenExchangeError = (e) => e instanceof Error && e.message.startsWith('xAI token exchange failed:');\nconst isInvalidGrant = (e) => isTokenExchangeError(e) && /invalid_grant/.test(e.message);","tryCatchPattern":"try { tokens = await xai.exchangeToken(cfg, code, redirectUri, verifier); }\ncatch (e) {\n  if (isInvalidGrant(e)) startNewOAuthFlow();      // code is single-use — never retry it\n  else if (isTokenExchangeError(e)) logAndAlert(e.message); // bad client config or endpoint moved\n  else throw e;\n}","preventionTips":["Never retry an authorization code — it is single-use; restart the flow instead","Persist the PKCE verifier across the whole flow (no process restarts mid-exchange)","Use the byte-identical redirect_uri in buildAuthUrl and exchangeToken","Re-fetch endpoint discovery if xAI rotates endpoints; don't cache stale token URLs"],"tags":["oauth","pkce","xai","token-exchange"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}