{"record":{"id":"1a9530ea18b50f19","repo":"medusajs/medusa","slug":"could-not-exchange-token-r-status-r-statust-1a9530","errorCode":null,"errorMessage":"Could not exchange token, ${r.status}, ${r.statusText}","messagePattern":"Could not exchange token, (.+?), (.+?)","errorType":"exception","errorClass":"MedusaError","httpStatus":400,"severity":"error","filePath":"packages/modules/providers/auth-google/src/services/google.ts","lineNumber":150,"sourceCode":"      return { success: false, error: \"No code provided\" }\n    }\n\n    const state = await authIdentityService.getState(query?.state as string)\n    if (!state) {\n      return { success: false, error: \"No state provided, or session expired\" }\n    }\n\n    const params = `client_id=${this.config_.clientId}&client_secret=${this.config_.clientSecret}&code=${code}&redirect_uri=${state.callback_url}&grant_type=authorization_code`\n    const exchangeTokenUrl = new URL(\n      `https://oauth2.googleapis.com/token?${params}`\n    )\n\n    try {\n      const response = await fetch(exchangeTokenUrl.toString(), {\n        method: \"POST\",\n      }).then((r) => {\n        if (!r.ok) {\n          throw new MedusaError(\n            MedusaError.Types.INVALID_DATA,\n            `Could not exchange token, ${r.status}, ${r.statusText}`\n          )\n        }\n\n        return r.json()\n      })\n\n      const { authIdentity, success } = await this.verify_(\n        response.id_token as string,\n        authIdentityService\n      )\n\n      return {\n        success,\n        authIdentity,\n      }\n    } catch (error) {","sourceCodeStart":132,"sourceCodeEnd":168,"githubUrl":"https://github.com/medusajs/medusa/blob/5e06e544a296b9033f20f71f11c559f81a0e5739/packages/modules/providers/auth-google/src/services/google.ts#L132-L168","documentation":"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.","triggerScenarios":"Expired or already-used authorization code, invalid client_secret/client_id, or redirect_uri mismatch between the auth request and the token exchange.","commonSituations":"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.","solutions":["Verify clientSecret and callbackUrl match the Google Cloud OAuth client configuration exactly","Restart the flow to obtain a fresh authorization code (single-use, ~10 min TTL)","Check Google Cloud Console for the client's status and any consent-screen misconfiguration"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"if (!req.query.code || req.query.error) {\n  return res.redirect('/auth/google/google') // fresh code instead of failing exchange\n}","typeGuard":null,"tryCatchPattern":"try { await provider.validateCallback(req.query) } catch (e) { if (e.type === 'invalid_data') res.redirect(loginUrl) else throw e }","preventionTips":["Keep client credentials and redirect URI synced with Google Cloud Console","Restart the flow on exchange failure; codes are single-use","Set sane session/state TTLs so users don't sit on stale codes"],"tags":["google","oauth","token-exchange","http-error"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"5e06e544a296b9033f20f71f11c559f81a0e5739","analyzedAt":"2026-08-27T07:24:39.599Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}