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
- Read the embedded error body in the thrown message — it names the OAuth error (invalid_grant, invalid_client, etc.) — and fix that specific cause.
- Ensure the redirectUri used at token exchange exactly matches the one in the authorization request and is registered in Clerk.
- Verify oauthClientId/oauthClientSecret belong to the correct Clerk instance/environment.
- 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.
- 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
- Redirect users away from the callback URL after success so authorization codes are never replayed.
- Keep redirect_uri identical between authorize and token requests and registered in Clerk.
- Retry only transient failures (5xx/timeouts); restart the flow for invalid_grant.
- Monitor Clerk status and set alerts on token-exchange failure rates.
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
- Failed to fetch user info from Clerk
- Token exchange failed: ${error}
- Redirect URI is required for SSO login
- Google service account token request failed (${response.stat
- Token exchange failed: ${error}
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/ae2f1f2a235c35b3.
Report an issue: GitHub.