{"record":{"id":"a071a10c2b17a773","repo":"gitroomhq/postiz-app","slug":"token-request-failed-error-a071a1","errorCode":null,"errorMessage":"Token request failed: ${error}","messagePattern":"Token request failed: (.+?)","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/backend/src/services/auth/providers/oauth.provider.ts","lineNumber":71,"sourceCode":"    const { tokenUrl, clientId, clientSecret, frontendUrl } = this.getConfig();\n    const response = await fetch(`${tokenUrl}`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n        Accept: 'application/json',\n      },\n      body: new URLSearchParams({\n        grant_type: 'authorization_code',\n        client_id: clientId,\n        client_secret: clientSecret,\n        code,\n        redirect_uri: `${frontendUrl}/settings`,\n      }),\n    });\n\n    if (!response.ok) {\n      const error = await response.text();\n      throw new Error(`Token request failed: ${error}`);\n    }\n\n    const { access_token } = await response.json();\n    return access_token;\n  }\n\n  async getUser(access_token: string): Promise<{ email: string; id: string }> {\n    const { userInfoUrl } = this.getConfig();\n    const response = await fetch(`${userInfoUrl}`, {\n      headers: {\n        Authorization: `Bearer ${access_token}`,\n        Accept: 'application/json',\n      },\n    });\n\n    if (!response.ok) {\n      const error = await response.text();\n      throw new Error(`User info request failed: ${error}`);","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/apps/backend/src/services/auth/providers/oauth.provider.ts#L53-L89","documentation":"Generic OAuth provider base class: the provider's token endpoint returned a non-2xx while exchanging the authorization code for an access_token. The provider's response body is appended, typically containing error=invalid_client/invalid_grant/redirect_uri_mismatch.","triggerScenarios":"Posting the code + client credentials to the provider's token URL with a wrong client_secret, an expired or already-used code, or a redirect_uri (`${frontendUrl}/settings`) that doesn't match what's registered on the provider app.","commonSituations":"Provider client secret rotated but env not updated; FRONTEND_URL env wrong or changed (http vs https, trailing slash) so redirect_uri mismatches; code used twice (double callback, retry logic); provider app in sandbox/test mode.","solutions":["Read the provider's error body in the message: invalid_client -> fix secret, invalid_grant -> fresh code, redirect_uri mismatch -> fix registration","Verify FRONTEND_URL exactly matches the redirect URI registered with the provider (scheme, host, path /settings)","Regenerate/re-verify CLIENT_ID and CLIENT_SECRET env values for that provider","Ensure the code is exchanged exactly once, immediately after callback"],"exampleFix":"# before\nFRONTEND_URL=http://localhost:4200\n\n# after — must match the provider-registered callback exactly\nFRONTEND_URL=https://app.example.com","handlingStrategy":"try-catch","validationCode":"null","typeGuard":"null","tryCatchPattern":"try {\n  const token = await provider.getToken(code);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Token request failed:')) {\n    const body = e.message.slice('Token request failed:'.length);\n    if (body.includes('redirect_uri')) fixRedirectUriRegistration();\n    else if (body.includes('invalid_grant')) restartFlow();\n    else throw new ConfigurationError('provider credentials', body);\n  }\n  throw e;\n}","preventionTips":["Keep FRONTEND_URL in sync with the registered redirect URI","Store client secrets in env and rotate on schedule","Exchange each code exactly once, right after callback"],"tags":["oauth","token-exchange","provider"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}