{"record":{"id":"2315c2700c308bbe","repo":"Budibase/budibase","slug":"teams-graph-api-resp-status-await-resp-text","errorCode":null,"errorMessage":"Teams Graph API ${resp.status}: ${await resp.text()}","messagePattern":"Teams Graph API (.+?): (.+?)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/server/src/escalation/notifications/ms-teams.ts","lineNumber":95,"sourceCode":"        )\n      }\n      const data = (await resp.json()) as {\n        access_token: string\n        expires_in?: number\n      }\n      return { value: data.access_token, ttl: data.expires_in ?? 3600 }\n    }\n  )\n}\n\nconst GRAPH_BASE = \"https://graph.microsoft.com/v1.0\"\n\nconst graphGet = async <T>(url: string, token: string): Promise<T> => {\n  const resp = await fetch(url, {\n    headers: { Authorization: `Bearer ${token}` },\n  })\n  if (!resp.ok) {\n    throw new Error(`Teams Graph API ${resp.status}: ${await resp.text()}`)\n  }\n  return (await resp.json()) as T\n}\n\n// Opaque base64url Graph nextLink. Validate origin + exact pathname so the\n// Graph token can only ever be sent to the teams collection.\nconst decodeTeamsCursor = (cursor: string): string => {\n  const decoded = Buffer.from(cursor, \"base64url\").toString()\n  let url: URL\n  try {\n    url = new URL(decoded)\n  } catch {\n    throw new HTTPError(\"Invalid cursor\", 400)\n  }\n  if (\n    url.origin !== \"https://graph.microsoft.com\" ||\n    url.pathname !== \"/v1.0/teams\"\n  ) {","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/escalation/notifications/ms-teams.ts#L77-L113","documentation":"graphGet is the generic wrapper for Microsoft Graph GET calls (used by listTeamsChannels to list teams and channels). Any non-ok response from Graph is rethrown as this error containing the HTTP status and the Graph error body. The token used is an app-only Graph token obtained with the https://graph.microsoft.com/.default scope.","triggerScenarios":"A Graph request made through graphGet returns a non-2xx status: expired/invalid bearer token (401), missing consented application permissions like Team.ReadBasic.All or Channel.ReadBasic.All (403), a malformed URL, or Graph throttling (429).","commonSituations":"Admin has not consented to the Graph application permissions; the app registration only has Bot Framework permissions but not Graph ones; Graph token cached past expiry due to clock skew; hitting rate limits while enumerating many teams; tenant restrictions blocking graph.microsoft.com.","solutions":["Read the status/body in the message: 401 → refresh/re-obtain the Graph token; 403 → grant admin consent for Team.ReadBasic.All and Channel.ReadBasic.All in Azure AD.","Verify the OAuth app has the required Graph application permissions (not delegated) and admin consent was granted.","If status is 429, retry with backoff and honor the Retry-After header.","Confirm the tenant allows service-principal access to Graph and the network can reach graph.microsoft.com."],"exampleFix":"// before\n// Azure portal: app has only Bot permissions → graphGet returns 403\n// after\n// App registration → API permissions → add application permissions:\n//   Team.ReadBasic.All, Channel.ReadBasic.All → grant admin consent","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const page = await graphGet(url, graphToken)\n} catch (err) {\n  if (err.message.startsWith(\"Teams Graph API 429\")) {\n    await sleep(backoff)\n    return retry()\n  }\n  if (err.message.startsWith(\"Teams Graph API 401\")) {\n    graphToken = await getOAuthToken(...) // refresh token\n    return retry()\n  }\n  throw err\n}","preventionTips":["Grant and verify admin consent for Team.ReadBasic.All and Channel.ReadBasic.All application permissions","Refresh Graph tokens proactively based on expires_in rather than on failure","Implement exponential backoff for Graph 429 responses honoring Retry-After"],"tags":["microsoft-graph","http","permissions","teams","network"],"backgroundTag":"graph-api-request-failed","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}