decolua/9router · error

xAI token exchange failed: ${err}

Error message

xAI token exchange failed: ${err}

What it means

exchangeXaiCode swaps an OAuth authorization code (with PKCE verifier) for tokens at xAI's token endpoint. When the HTTP response is not ok, the raw response body text is read and thrown verbatim inside 'xAI token exchange failed: <body>'. The body usually contains xAI's OAuth error JSON (e.g. invalid_grant, invalid_client).

Source

Thrown at src/lib/oauth/services/xai.js:147

  async exchangeXaiCode({ tokenUrl, code, redirectUri, codeVerifier }) {
    const res = await fetch(tokenUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        Accept: "application/json",
      },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        client_id: XAI_CONFIG.clientId,
        code,
        redirect_uri: redirectUri,
        code_verifier: codeVerifier,
      }),
    });

    if (!res.ok) {
      const err = await res.text();
      throw new Error(`xAI token exchange failed: ${err}`);
    }
    return await res.json();
  }

  /**
   * Refresh an access token using a refresh_token.
   */
  async refreshAccessToken(refreshToken) {
    const { tokenUrl } = await discoverEndpoints();
    const res = await fetch(tokenUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        Accept: "application/json",
      },
      body: new URLSearchParams({
        grant_type: "refresh_token",
        client_id: XAI_CONFIG.clientId,

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Parse the thrown body text — it typically contains 'error' (e.g. invalid_grant) pinpointing the cause.
  2. For invalid_grant, restart the OAuth flow: authorization codes are single-use and short-lived.
  3. Verify client_id, redirect_uri, and the code_verifier match exactly what was used in the authorize step.
  4. If the exchange failed after the code was consumed, do not blindly retry — request a fresh code.

Example fix

// before
const tokens = await exchangeXaiCode(code, verifier); // throws on 400
// after
try {
  const tokens = await exchangeXaiCode(code, verifier);
} catch (e) {
  if (e.message.includes('invalid_grant')) return startNewAuthorization();
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (typeof code !== 'string' || !code || typeof codeVerifier !== 'string' || !codeVerifier) {
  throw new Error('cannot exchange: missing code or verifier');
}

Try / catch

try {
  const tokens = await exchangeXaiCode(code, codeVerifier);
} catch (e) {
  if (!e.message.startsWith('xAI token exchange failed:')) throw e;
  const body = e.message.slice('xAI token exchange failed:'.length);
  if (body.includes('invalid_grant')) return restartAuthorizationFlow(); // code expired/used
  if (body.includes('invalid_client')) throw new Error('check XAI client_id/credentials');
  throw e;
}

Prevention

When it happens

Trigger: POST to the xAI token endpoint returns 400/401/etc. — expired or already-redeemed authorization code, wrong code_verifier (PKCE mismatch), invalid client_id, or revoked/blocked client.

Common situations: User taking too long between authorize and exchange (code TTL expiry); exchanging the same code twice after a retry; mismatched redirect_uri or client credentials between the authorize request and the exchange.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/b50084386acaa407. Report an issue: GitHub.