decolua/9router · error · Error

`Token exchange failed: ${error}`

Error message

`Token exchange failed: ${error}`

What it means

claude.js exchangeToken performs the PKCE authorization-code exchange with Anthropic's token endpoint; any non-ok response triggers this error carrying the raw response body. Common OAuth failure codes (invalid_grant, invalid_client, redirect_uri_mismatch) plus state/verifier mismatches surface here.

Source

Thrown at src/lib/oauth/providers/claude.js:47

    const response = await fetch(config.tokenUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
      },
      body: JSON.stringify({
        code: authCode,
        state: codeState || state,
        grant_type: "authorization_code",
        client_id: config.clientId,
        redirect_uri: redirectUri,
        code_verifier: codeVerifier,
      }),
    });

    if (!response.ok) {
      const error = await response.text();
      throw new Error(`Token exchange failed: ${error}`);
    }

    return await response.json();
  },
  mapTokens: (tokens) => ({
    accessToken: tokens.access_token,
    refreshToken: tokens.refresh_token,
    expiresIn: tokens.expires_in,
    scope: tokens.scope,
  }),
};

export default claude;

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Inspect the appended body for the OAuth error code and address it directly (invalid_grant -> restart flow).
  2. invalid_grant: codes are single-use and short-lived; restart the login from buildAuthUrl.
  3. Ensure the same codeVerifier generated for the auth URL is passed to exchangeToken (don't regenerate between steps).
  4. Match redirect_uri exactly between buildAuthUrl and exchangeToken, including port and scheme.
  5. If 5xx, retry the full flow after a short wait.

Example fix

// before: fresh verifier at exchange time kills PKCE
const verifier = generateVerifier(); // different from the one in the auth URL
await exchangeToken(config, code, redirectUri, verifier, state);
// after: persist the original verifier
await exchangeToken(config, code, redirectUri, pkce.verifier, state);
Defensive patterns

Strategy: try-catch

Validate before calling

if (!code || !codeVerifier || !redirectUri) throw new Error('PKCE exchange requires code, codeVerifier and redirectUri');
// code may carry '#state' — that is handled internally, do not pre-strip differently

Type guard

function isPkceExchangeInput(v) {
  return typeof v?.code === 'string' && v.code.length > 0 &&
         typeof v?.codeVerifier === 'string' && v.codeVerifier.length >= 43 &&
         typeof v?.redirectUri === 'string' && /^https?:\/\//.test(v.redirectUri);
}

Try / catch

try {
  const tokens = await claude.exchangeToken(config, code, redirectUri, pkce.verifier, state);
  // use tokens
} catch (err) {
  if (String(err.message).startsWith('Token exchange failed:')) {
    if (/invalid_grant/.test(err.message)) restartLogin();       // replayed/expired code or verifier mismatch
    else if (/invalid_client/.test(err.message)) updateClaudeConfig();
    else if (/redirect_uri_mismatch/.test(err.message)) alignRedirectUri();
    else backoffAndRetry();
  } else throw err;
}

Prevention

When it happens

Trigger: Exchanging a code whose PKCE code_verifier doesn't match the challenge used in buildAuthUrl, replaying a consumed/expired code, passing the code including the '#state' suffix (it is split, but a state mismatch still fails server-side), wrong clientId in CLAUDE_CONFIG, or redirect_uri differing between authorize and exchange.

Common situations: Anthropic rotating its OAuth client config, users completing the flow twice in two tabs, code pasted from a browser where the fragment was truncated, clock skew expiring the code, or CI environments where the callback port differs from the auth URL.

Related errors


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