Budibase/budibase · error · Error

Teams Graph API ${resp.status}: ${await resp.text()}

Error message

Teams Graph API ${resp.status}: ${await resp.text()}

What it means

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.

Source

Thrown at packages/server/src/escalation/notifications/ms-teams.ts:95

        )
      }
      const data = (await resp.json()) as {
        access_token: string
        expires_in?: number
      }
      return { value: data.access_token, ttl: data.expires_in ?? 3600 }
    }
  )
}

const GRAPH_BASE = "https://graph.microsoft.com/v1.0"

const graphGet = async <T>(url: string, token: string): Promise<T> => {
  const resp = await fetch(url, {
    headers: { Authorization: `Bearer ${token}` },
  })
  if (!resp.ok) {
    throw new Error(`Teams Graph API ${resp.status}: ${await resp.text()}`)
  }
  return (await resp.json()) as T
}

// Opaque base64url Graph nextLink. Validate origin + exact pathname so the
// Graph token can only ever be sent to the teams collection.
const decodeTeamsCursor = (cursor: string): string => {
  const decoded = Buffer.from(cursor, "base64url").toString()
  let url: URL
  try {
    url = new URL(decoded)
  } catch {
    throw new HTTPError("Invalid cursor", 400)
  }
  if (
    url.origin !== "https://graph.microsoft.com" ||
    url.pathname !== "/v1.0/teams"
  ) {

View on GitHub (pinned to a81a902e9a)

Solutions

  1. 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.
  2. Verify the OAuth app has the required Graph application permissions (not delegated) and admin consent was granted.
  3. If status is 429, retry with backoff and honor the Retry-After header.
  4. Confirm the tenant allows service-principal access to Graph and the network can reach graph.microsoft.com.

Example fix

// before
// Azure portal: app has only Bot permissions → graphGet returns 403
// after
// App registration → API permissions → add application permissions:
//   Team.ReadBasic.All, Channel.ReadBasic.All → grant admin consent
Defensive patterns

Strategy: retry

Try / catch

try {
  const page = await graphGet(url, graphToken)
} catch (err) {
  if (err.message.startsWith("Teams Graph API 429")) {
    await sleep(backoff)
    return retry()
  }
  if (err.message.startsWith("Teams Graph API 401")) {
    graphToken = await getOAuthToken(...) // refresh token
    return retry()
  }
  throw err
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of Budibase/budibase@a81a902e9a (2026-08-29). Data as JSON: /api/errors/2315c2700c308bbe. Report an issue: GitHub.