medusajs/medusa · error · MedusaError

Could not exchange token, ${r.status}, ${r.statusText}

Error message

Could not exchange token, ${r.status}, ${r.statusText}

What it means

In the Google OAuth callback, the provider POSTs the authorization code to Google's token endpoint. A non-2xx response triggers INVALID_DATA with the HTTP status and reason.

Source

Thrown at packages/modules/providers/auth-google/src/services/google.ts:150

      return { success: false, error: "No code provided" }
    }

    const state = await authIdentityService.getState(query?.state as string)
    if (!state) {
      return { success: false, error: "No state provided, or session expired" }
    }

    const params = `client_id=${this.config_.clientId}&client_secret=${this.config_.clientSecret}&code=${code}&redirect_uri=${state.callback_url}&grant_type=authorization_code`
    const exchangeTokenUrl = new URL(
      `https://oauth2.googleapis.com/token?${params}`
    )

    try {
      const response = await fetch(exchangeTokenUrl.toString(), {
        method: "POST",
      }).then((r) => {
        if (!r.ok) {
          throw new MedusaError(
            MedusaError.Types.INVALID_DATA,
            `Could not exchange token, ${r.status}, ${r.statusText}`
          )
        }

        return r.json()
      })

      const { authIdentity, success } = await this.verify_(
        response.id_token as string,
        authIdentityService
      )

      return {
        success,
        authIdentity,
      }
    } catch (error) {

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Verify clientSecret and callbackUrl match the Google Cloud OAuth client configuration exactly
  2. Restart the flow to obtain a fresh authorization code (single-use, ~10 min TTL)
  3. Check Google Cloud Console for the client's status and any consent-screen misconfiguration
Defensive patterns

Strategy: retry

Validate before calling

if (!req.query.code || req.query.error) {
  return res.redirect('/auth/google/google') // fresh code instead of failing exchange
}

Try / catch

try { await provider.validateCallback(req.query) } catch (e) { if (e.type === 'invalid_data') res.redirect(loginUrl) else throw e }

Prevention

When it happens

Trigger: Expired or already-used authorization code, invalid client_secret/client_id, or redirect_uri mismatch between the auth request and the token exchange.

Common situations: User refreshing or re-visiting the callback URL after the code was consumed, secrets rotated since the flow started, or callbackUrl not matching the registered redirect URI in Google Cloud Console.

Related errors


AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27). Data as JSON: /api/errors/1a9530ea18b50f19. Report an issue: GitHub.