mastra-ai/mastra · error
Google token exchange failed: ${error}
Error message
Google token exchange failed: ${error} What it means
Thrown by MastraAuthGoogle.handleCallback when the POST to Google's OAuth token endpoint (https://oauth2.googleapis.com/token) returns a non-OK HTTP status during the authorization-code exchange. The response body (Google's JSON error, e.g. invalid_grant) is appended to the message. It means Google rejected the code/credentials/redirect_uri combination, so no tokens were issued.
Source
Thrown at auth/google/src/auth-provider.ts:490
const { originalState, redirectUri, nonce } = await verifyStateToken(signedState, self.cookiePassword);
verifyCallbackStateSuffix(callbackState, originalState);
const tokenResponse = await fetch(GOOGLE_TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: self.clientId,
client_secret: self.clientSecret!,
redirect_uri: redirectUri,
}),
signal: AbortSignal.timeout(10_000),
});
if (!tokenResponse.ok) {
const error = await tokenResponse.text();
throw new Error(`Google 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;
};
if (!tokens.id_token) {
throw new Error('Google token response did not include an ID token');
}
const user = await self.verifyIdToken(tokens.id_token, nonce);
const sessionData = {
user,
expiresAt: Date.now() + self.cookieMaxAge * 1000,View on GitHub (pinned to 75dd419e61)
Solutions
- Verify the redirect_uri used in getAuthorizationUrl exactly matches the one sent to the token exchange and is registered in Google Cloud Console (OAuth client authorized redirect URIs).
- Check GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are correct for the same OAuth client and environment.
- Don't retry with a consumed code: the code is single-use (invalid_grant); restart the OAuth flow with a fresh authorization URL.
- Read the appended Google error body in the message to identify the exact OAuth error code (invalid_client, invalid_grant, redirect_uri_mismatch, etc.).
- If 5xx/internal errors, retry the full OAuth flow; Google may have transient issues.
Example fix
// before (common bug: different redirect URI at token exchange)
const tokens = await exchange(code, { redirect_uri: 'https://prod.example.com/callback' });
// after: reuse the exact URI from the authorize step
const redirectUri = self.redirectUri; // same value put in the authorize URL
const tokens = await exchange(code, { redirect_uri: redirectUri }); Defensive patterns
Strategy: try-catch
Validate before calling
if (!clientId || !clientSecret) throw new Error('Google OAuth client credentials required before starting SSO');
const redirectUri = new URL(callbackUrl, baseUrl).toString();
if (!registeredRedirectUris.includes(redirectUri)) throw new Error(`redirect_uri ${redirectUri} not registered in Google Cloud Console`); Type guard
function isGoogleTokenError(err: unknown): err is Error & { message: string } {
return err instanceof Error && err.message.startsWith('Google token exchange failed:');
} Try / catch
try {
const result = await provider.handleCallback(code, state);
} catch (err) {
if (err instanceof Error && err.message.startsWith('Google token exchange failed:')) {
// log err.message (contains Google's error body: invalid_grant, redirect_uri_mismatch, ...)
// code is single-use — redirect the user to a fresh authorization URL; never reuse `code`
return redirectTo(provider.getAuthorizationUrl(state));
}
throw err;
} Prevention
- Keep a single source of truth for redirect_uri and use it in both the authorize and token steps
- Register all environment callback URLs in Google Cloud Console
- Never retry with the same authorization code; codes are single-use
- Check GOOGLE_CLIENT_SECRET per environment before deploy
- Surface the appended Google error body in logs to speed diagnosis
When it happens
Trigger: Calling handleCallback(code, callbackState) when Google returns an error status: the authorization code was already redeemed or expired (invalid_grant), client_id/client_secret are wrong (invalid_client), the redirect_uri differs from the one used in the authorization URL, the code was tampered with, or Google is returning 5xx.
Common situations: Replaying a callback (browser refresh double-redeems the code); mismatched redirect URI between the authorize step and the token exchange (e.g. localhost vs production domain not registered in Google Cloud Console); wrong GOOGLE_CLIENT_SECRET or swapped env values between environments; clock skew/expired code from slow redirects.
Related errors
- Redirect URI is required for Google SSO. Set GOOGLE_REDIRECT
- Google token response did not include an ID token
- Google service account token request failed (${response.stat
- Token exchange failed: ${error}
- Redirect URI is required for SSO. Set AUTH0_REDIRECT_URI or
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/347f6fd40bb5040e.
Report an issue: GitHub.