mastra-ai/mastra · error

Token exchange failed: ${error}

Error message

Token exchange failed: ${error}

What it means

After the OAuth redirect, the provider exchanges the authorization code for tokens at Clerk's OAuth token endpoint using client credentials, with a 10-second timeout. Any non-2xx response (invalid code, expired code, wrong client secret, redirect_uri mismatch, Clerk outage) causes the library to read the response body and throw `Token exchange failed: <body>`, surfacing the upstream OAuth error text to the caller.

Source

Thrown at auth/clerk/src/index.ts:566

      // Exchange code for tokens using client_secret (confidential client)
      const tokenResponse = await fetch(`${self.fapiUrl}/oauth/token`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          Authorization: `Basic ${btoa(`${self.oauthClientId}:${self.oauthClientSecret}`)}`,
        },
        body: new URLSearchParams({
          grant_type: 'authorization_code',
          code,
          redirect_uri: redirectUri,
        }),
        signal: AbortSignal.timeout(10_000), // 10 second timeout
      });

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

      const tokens = (await tokenResponse.json()) as {
        access_token: string;
        id_token?: string;
        refresh_token?: string;
        expires_in: number;
        token_type: string;
      };

      // Get user info — try ID token first, fall back to userinfo endpoint
      let user: EEUser;
      if (tokens.id_token) {
        const payload = await verifyJwks(tokens.id_token, self.jwksUri);
        user = {
          id: payload.sub!,
          email: (payload.email as string) ?? undefined,
          name: (payload.name as string) ?? undefined,

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Read the embedded error body in the thrown message — it names the OAuth error (invalid_grant, invalid_client, etc.) — and fix that specific cause.
  2. Ensure the redirectUri used at token exchange exactly matches the one in the authorization request and is registered in Clerk.
  3. Verify oauthClientId/oauthClientSecret belong to the correct Clerk instance/environment.
  4. Handle replays: redirect users away from the callback URL after success so the code isn't reused; consider a single retry for transient 5xx/network errors.
  5. If message indicates timeout, check connectivity to Clerk's FAPI endpoint from your deployment.

Example fix

// before: assume every failure is transient
return handleCallback(req);
// after
try {
  return await handleCallback(req);
} catch (e) {
  if (String((e as Error).message).includes('invalid_grant')) {
    return res.redirect('/auth/sso/login'); // restart OAuth, code is single-use
  }
  throw e;
}
Defensive patterns

Strategy: retry

Try / catch

try {
  return await handleSsoCallback(req);
} catch (e) {
  const msg = (e as Error).message;
  if (msg.startsWith('Token exchange failed:')) {
    if (msg.includes('invalid_grant')) return restartLoginFlow(); // replayed/expired code
    if (msg.includes('invalid_client')) throw new Error('Check oauthClientId/secret configuration');
    if (msg.includes('timeout') || msg.includes('5')) return retryWithBackoff(handleSsoCallback, req, 2);
  }
  throw e;
}

Prevention

When it happens

Trigger: The SSO callback handler runs the code-for-token fetch and Clerk replies with !tokenResponse.ok — e.g. error=invalid_grant (code already used or expired), error=invalid_client (bad oauthClientSecret), or redirect_uri not matching the one used in the authorization request.

Common situations: User refreshes the callback page, replaying a one-time authorization code; mismatched redirect URI between authorize and token requests; wrong Clerk OAuth client secret per environment; network/proxy issues or Clerk downtime; race where two tabs complete the flow simultaneously.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/ae2f1f2a235c35b3. Report an issue: GitHub.