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

  1. 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).
  2. Check GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are correct for the same OAuth client and environment.
  3. Don't retry with a consumed code: the code is single-use (invalid_grant); restart the OAuth flow with a fresh authorization URL.
  4. Read the appended Google error body in the message to identify the exact OAuth error code (invalid_client, invalid_grant, redirect_uri_mismatch, etc.).
  5. 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

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


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