{"record":{"id":"c65565ea7e1f1bf6","repo":"medusajs/medusa","slug":"could-not-exchange-token-r-status-r-statust-c65565","errorCode":null,"errorMessage":"Could not exchange token, ${r.status}, ${r.statusText}","messagePattern":"Could not exchange token, (.+?), (.+?)","errorType":"validation","errorClass":"MedusaError","httpStatus":400,"severity":"error","filePath":"packages/modules/providers/auth-github/src/services/github.ts","lineNumber":119,"sourceCode":"    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}`\n\n    const exchangeTokenUrl = new URL(\n      `https://github.com/login/oauth/access_token?${params}`\n    )\n\n    try {\n      const response = await fetch(exchangeTokenUrl.toString(), {\n        method: \"POST\",\n        headers: {\n          Accept: \"application/json\",\n        },\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 providerMetadata = {\n        access_token: response.access_token,\n        refresh_token: response.refresh_token,\n        // The response is in seconds\n        access_token_expires_at: new Date(\n          Date.now() + response.expires_in * 1000\n        ).toISOString(),\n        refresh_token_expires_at: new Date(\n          Date.now() + response.refresh_token_expires_in * 1000\n        ).toISOString(),","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/medusajs/medusa/blob/5e06e544a296b9033f20f71f11c559f81a0e5739/packages/modules/providers/auth-github/src/services/github.ts#L101-L137","documentation":"During the Github OAuth callback, the provider exchanges the authorization code for an access token at Github's token endpoint. If Github returns a non-2xx response, the provider throws INVALID_DATA with the HTTP status and reason.","triggerScenarios":"The fetch to `https://github.com/login/oauth/access_token` failing: expired/reused authorization code, mismatched client secret, or mismatched redirect_uri.","commonSituations":"Replaying or refreshing a callback URL after the one-time code was consumed, wrong clientSecret in config, or callbackUrl differing from the OAuth app's registered callback URL.","solutions":["Verify clientSecret and callbackUrl in the provider options exactly match the Github OAuth app settings","Retry the full OAuth flow from the beginning to get a fresh authorization code (codes are single-use and short-lived)","Check Github's status page if exchanges fail consistently"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"if (!req.query.code) {\n  return res.redirect('/auth/github/github') // restart flow instead of exchanging a dead code\n}","typeGuard":null,"tryCatchPattern":"try { await provider.validateCallback(req.query) } catch (e) { if (e.type === 'invalid_data') res.redirect(loginUrl) else throw e } // restart flow on exchange failure","preventionTips":["Never allow the callback URL to be refreshed/replayed","Keep clientSecret and callbackUrl in sync with the Github app","Handle 'exchange failed' by restarting the OAuth flow, not by retrying the same code"],"tags":["github","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"}