{"record":{"id":"535ba76e5d8b3184","repo":"usebruno/bruno","slug":"access-token-url-is-required-for-oauth2","errorCode":null,"errorMessage":"Access token URL is required for OAuth2","messagePattern":"Access token URL is required for OAuth2","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/bruno-requests/src/auth/oauth2-helper.ts","lineNumber":337,"sourceCode":"export const getOAuth2Token = async (oauth2Config: OAuth2Config, tokenStore: TokenStore, verbose: string, axiosInstance?: AxiosInstance): Promise<string | null> => {\n  const {\n    grantType,\n    accessTokenUrl,\n    credentialsId = 'default',\n    autoFetchToken = true,\n    tokenSource = 'access_token'\n  } = oauth2Config;\n\n  if (verbose) {\n    debug.enable('oauth2');\n  }\n\n  if (!grantType) {\n    throw new Error('Grant type is required for OAuth2');\n  }\n\n  if (!accessTokenUrl) {\n    throw new Error('Access token URL is required for OAuth2');\n  }\n\n  if (!['client_credentials', 'password'].includes(grantType)) {\n    throw new Error(`Unsupported grant type: ${grantType}. Supported types: client_credentials, password`);\n  }\n\n  // Check if we already have credentials stored\n  const existingToken = await tokenStore.getCredential({ url: accessTokenUrl, credentialsId });\n\n  if (existingToken) {\n    // Check if token is expired\n    if (!isTokenExpired(existingToken)) {\n      // Token is valid, use it\n      return tokenSource === 'id_token' ? existingToken.id_token : existingToken.access_token;\n    } else {\n      // Token is expired\n      if (autoFetchToken) {\n        // Clear expired token and proceed to fetch new token","sourceCodeStart":319,"sourceCodeEnd":355,"githubUrl":"https://github.com/usebruno/bruno/blob/9bdd81c7bdc57006e5f5ebffb79321a8d979f712/packages/bruno-requests/src/auth/oauth2-helper.ts#L319-L355","documentation":"Thrown by getOAuth2AccessToken when grantType is present but accessTokenUrl is falsy. This is the top-level URL check, distinct from the per-flow checks at lines 122/215; it runs after the grant-type check and before the supported-grant-type check, so it catches the omission regardless of flow.","triggerScenarios":"Calling getOAuth2AccessToken with a valid grantType but no accessTokenUrl. Note this fires before flow-specific helpers, so it pre-empts errors 380 and 382.","commonSituations":"Config built from a template that left the token URL as a placeholder; env var for the URL not set in the deployed environment; the URL was stripped by a sanitizer.","solutions":["Populate accessTokenUrl with the authorization server's token endpoint.","If the URL is templated, resolve it (env substitution) before passing the config to the helper.","Add an assertion at config build time so missing URLs fail loudly and early."],"exampleFix":"// before\nconst config = { grantType: 'client_credentials', clientId: id, clientSecret: secret };\n\n// after\nconst config = {\n  grantType: 'client_credentials',\n  accessTokenUrl: process.env.OAUTH_TOKEN_URL || 'https://auth.example.com/oauth/token',\n  clientId: id,\n  clientSecret: secret\n};","handlingStrategy":"validation","validationCode":"if (!config.accessTokenUrl) throw new Error('accessTokenUrl must be set on OAuth2Config');","typeGuard":"function hasAccessTokenUrl(c) { return typeof c.accessTokenUrl === 'string' && c.accessTokenUrl.length > 0; }","tryCatchPattern":"try { await getOAuth2AccessToken(config, tokenStore); }\ncatch (e) { if (e.message === 'Access token URL is required for OAuth2') { /* set URL */ } else throw e; }","preventionTips":["Run env substitution on the URL before calling the helper.","Validate that the URL parses with new URL(...) to catch malformed values.","This top-level check pre-empts errors 380/382, so fixing it early removes a whole class of errors."],"tags":["oauth2","validation","configuration"],"backgroundTag":null,"analyzedSha":"9bdd81c7bdc57006e5f5ebffb79321a8d979f712","analyzedAt":"2026-08-13T04:09:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}