mastra-ai/mastra · error
Token exchange failed: ${error}
Error message
Token exchange failed: ${error} What it means
After validating the code and state, `completeAnthropicLogin` POSTs to Anthropic's token endpoint (console.anthropic.com/v1/oauth/token) with grant_type=authorization_code. If the HTTP response is not ok, it reads the response body as text and throws 'Token exchange failed: <body>'. The embedded text is Anthropic's own error payload (e.g. invalid_grant, invalid_client) and is the key to diagnosing the failure.
Source
Thrown at mastracode/sdk/src/auth/providers/anthropic.ts:91
// caller (and, in the shipyard server, the containing project lock)
// indefinitely. See 2025-07-23 shipyard latency incident.
signal: AbortSignal.timeout(15_000),
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
code,
state,
redirect_uri: REDIRECT_URI,
code_verifier: verifier,
}),
});
if (!tokenResponse.ok) {
const error = await tokenResponse.text();
throw new Error(`Token exchange failed: ${error}`);
}
const tokenData = (await tokenResponse.json()) as {
access_token: string;
refresh_token: string;
expires_in: number;
};
// Calculate expiry time (current time + expires_in seconds - 5 min buffer)
const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000;
return {
refresh: tokenData.refresh_token,
access: tokenData.access_token,
expires: expiresAt,
};
}
View on GitHub (pinned to 75dd419e61)
Solutions
- Read the embedded error text: invalid_grant usually means the code was consumed or expired — restart with startAnthropicLogin() and re-authorize; do not reuse the same code.
- Ensure each code is exchanged exactly once; make retries generate a fresh login instead of resubmitting the same code.
- Verify the verifier passed to completeAnthropicLogin is the one from the startAnthropicLogin() call that generated the authorization URL (PKCE binding).
- For 5xx/network errors, retry with fresh backoff only after confirming the code has not been redeemed; a redeemed code will never succeed again.
- Check outbound network/proxy access to console.anthropic.com; the request is bounded by a 15s timeout.
Example fix
// before
// retrying the exchange after a timeout reuses the consumed code
try {
await completeAnthropicLogin(input, verifier);
} catch {
await completeAnthropicLogin(input, verifier); // 'Token exchange failed: invalid_grant'
}
// after
try {
await completeAnthropicLogin(input, verifier);
} catch (e) {
// code is single-use: always restart the flow for a fresh code
const { url, verifier: v2 } = await startAnthropicLogin();
showAuthUrl(url);
const freshInput = await promptForCode();
await completeAnthropicLogin(freshInput, v2);
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-checks: code present and exchange not already attempted for this code
if (!code) throw new Error('No authorization code to exchange');
if (await exchangedCodes.has(code)) throw new Error('Code already redeemed — restart the login'); Type guard
function isTokenExchangeError(e: unknown): e is Error & { message: string } {
return e instanceof Error && e.message.startsWith('Token exchange failed:');
} Try / catch
try {
return await completeAnthropicLogin(input, verifier);
} catch (e) {
if (isTokenExchangeError(e)) {
if (/invalid_grant/i.test(e.message)) {
// code consumed or expired: single-use — restart the flow, never retry same code
return startFreshLogin();
}
if (/\b5\d\d\b|timeout|aborted/i.test(e.message)) {
await sleep(1000); // transient: safe retry only if code not yet redeemed
}
}
throw e;
} Prevention
- Treat authorization codes as strictly single-use; never retry an exchange with the same code after a failure.
- Complete the exchange promptly — codes expire within minutes of issuance.
- Keep the PKCE verifier from the exact startAnthropicLogin() call that produced the URL.
- Wrap the fetch with your own timeout/retry policy for 5xx, matching the built-in 15s AbortSignal.timeout.
- Log the embedded upstream error text to distinguish auth failures from transient outages.
When it happens
Trigger: The authorization code was already redeemed (invalid_grant) — e.g. a retry or two callers exchanging the same code; the code expired (Anthropic codes are short-lived); the PKCE code_verifier does not match the challenge sent at authorize time; a client_id/endpoint mismatch; or any 4xx/5xx from the token endpoint including timeouts raised by the built-in 15s AbortSignal.timeout.
Common situations: Double-exchange after a timeout retry; user waited too long between authorizing and pasting the code; copying the code from a stale browser tab started with an older verifier/challenge; Anthropic endpoint changes or temporary 5xx; proxy/firewall mangling the POST.
Related errors
- Missing authorization code
- Invalid authorization state
- Anthropic token refresh failed: ${error}
- Token exchange failed: ${error}
- Token exchange failed: ${error}
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/100bb39310f9fd09.
Report an issue: GitHub.