{"record":{"id":"dffca3e0e6d7c6cf","repo":"Budibase/budibase","slug":"error-fetching-oauth2-token-message","errorCode":null,"errorMessage":"Error fetching oauth2 token: ${message}","messagePattern":"Error fetching oauth2 token: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/server/src/sdk/workspace/oauth2/utils.ts","lineNumber":95,"sourceCode":"  await writethrough.patch({\n    lastUsage: Date.now(),\n  })\n}\n\nasync function fetchAndParseToken(config: {\n  url: string\n  clientId: string\n  clientSecret: string\n  method: OAuth2CredentialsMethod\n  grantType: OAuth2GrantType\n  scope?: string\n  audience?: string\n}): Promise<{ value: string; ttl: number }> {\n  const resp = await fetchToken(config)\n  const jsonResponse = await resp.json()\n  if (!resp.ok) {\n    const message = jsonResponse.error_description ?? resp.statusText\n    throw new Error(`Error fetching oauth2 token: ${message}`)\n  }\n  const token = `${jsonResponse.token_type} ${jsonResponse.access_token}`\n  const ttl = jsonResponse.expires_in ?? -1\n  return { value: token, ttl }\n}\n\nexport async function getToken(id: string) {\n  const token = await cache.withCacheWithDynamicTTL(\n    cache.CacheKey.OAUTH2_TOKEN(id),\n    async () => {\n      const config = await get(id)\n      if (!config) {\n        throw new HTTPError(`oAuth config ${id} could not be found`, 400)\n      }\n      return fetchAndParseToken(config)\n    }\n  )\n","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/sdk/workspace/oauth2/utils.ts#L77-L113","documentation":"fetchAndParseToken POSTs client-credentials to the OAuth2 token endpoint. If the provider responds with a non-OK status, the thrown plain Error surfaces the provider's error_description (or HTTP statusText). This means the token request itself was rejected by the identity provider, not that a Budibase config was missing.","triggerScenarios":"Calling getToken(id) (or getTokenFromConfig) where fetchToken receives a 4xx/5xx response: wrong client_id/client_secret, unsupported grant_type, invalid scope/audience, token URL pointing at a non-token endpoint, or the endpoint returning an error body that is not JSON-encodable errors.","commonSituations":"Expired or rotated client secrets after provider-side changes; Auth0-style providers requiring an audience parameter that was not set; scope names changed by the provider; environment-variable substitution producing an empty secret; firewalls/proxies returning HTML error pages that break resp.json().","solutions":["Verify clientId/clientSecret and grant type against the provider's settings","Check the token URL is the exact token endpoint (e.g. https://provider/oauth/token)","Ensure required scope/audience values are configured","Read the error_description in the message — it names the provider's exact rejection reason","Confirm env-var placeholders used in the config resolve to non-empty values"],"exampleFix":"// before\n// config.clientSecret stored as {{env.SECRET}} but env var missing\n// after\n// set the env var (or hardcode a valid secret in dev) and redeploy so\n// processEnvironmentVariable resolves a real secret before the token POST","handlingStrategy":"try-catch","validationCode":"if (!config.url || !config.clientId || !config.clientSecret) {\n  throw new Error('OAuth2 config incomplete: url, clientId and clientSecret are required')\n}","typeGuard":"null","tryCatchPattern":"try {\n  const token = await getToken(configId)\n} catch (e) {\n  if (/^Error fetching oauth2 token:/.test(String(e.message))) {\n    // provider rejected the request: log e.message (contains error_description),\n    // fix credentials/URL/scope, and apply backoff before retrying\n  } else throw e\n}","preventionTips":["Validate the token URL is the exact provider token endpoint","Rotate and re-test client secrets after provider changes","Set required audience/scope values for providers like Auth0","Verify env-var placeholders resolve to non-empty values before deploying"],"tags":["oauth2","http","token-endpoint","network"],"backgroundTag":"oauth-token-request-failed","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}