Budibase/budibase · error · Error

Slack OAuth callback is missing state

Error message

Slack OAuth callback is missing state

What it means

completeSlackOAuth reads the OAuth state parameter from the callback query. Slack redirects back with ?state=...; if it is absent or empty the handler throws a plain Error because the callback cannot be matched to a pending OAuth initiation stored in cache.

Source

Thrown at packages/server/src/api/controllers/ai/agents.ts:573

  })

  ctx.body = {
    success: true,
    appId,
    messagingEndpointUrl: slackAppDeployment.messagingEndpointUrl,
    oauthAuthorizeUrl: buildSlackInstallUrl({
      oauthAuthorizeUrl,
      redirectUri: oauthRedirectUrl,
      state,
    }),
  }
  ctx.status = 200
}

export async function completeSlackOAuth(ctx: UserCtx<void, void>) {
  const state = String(ctx.query.state || "").trim()
  if (!state) {
    throw new Error("Slack OAuth callback is missing state")
  }

  const cacheKey = getSlackOAuthStateCacheKey(state)
  const statePayload = (await cache.get(cacheKey, {
    useTenancy: false,
  })) as SlackOAuthState | undefined
  await cache.destroy(cacheKey, { useTenancy: false })
  if (!statePayload?.agentId || !statePayload.workspaceId) {
    throw new Error("Slack OAuth state is invalid or expired")
  }

  const oauthError = String(ctx.query.error || "").trim()
  if (oauthError) {
    throw new Error("Slack OAuth authorization failed")
  }

  const code = String(ctx.query.code || "").trim()
  if (!code) {

View on GitHub (pinned to a81a902e9a)

Solutions

  1. Restart the OAuth flow from the beginning (re-initiate so a fresh state is generated and embedded in the Slack authorize URL)
  2. Check that the redirect/authorize URL construction includes state and that the registered Slack redirect URL preserves query params
  3. Have the user use the original install link rather than a saved callback URL
  4. Confirm no reverse proxy or middleware is stripping query parameters

Example fix

// before
const state = String(ctx.query.state || "").trim()
// after (caller-side check before hitting the callback)
if (!url.searchParams.get("state")) {
  url.searchParams.set("state", generatedState)
}
Defensive patterns

Strategy: validation

Validate before calling

const state = new URL(callbackUrl).searchParams.get("state")
if (!state) {
  // restart the flow instead of calling the callback handler
  return initiateSlackOAuth(agentId)
}

Type guard

function hasOAuthState(q: Record<string, unknown>): q is Record<string, string> & { state: string } {
  return typeof q.state === "string" && q.state.trim() !== ""
}

Try / catch

try {
  await completeSlackOAuth(ctx)
} catch (err) {
  if (err.message.includes("missing state")) {
    // redirect the user to restart the Slack installation flow
  } else { throw err }
}

Prevention

When it happens

Trigger: User lands on the callback URL without a state query param — bookmarked/stale callback URL, manually edited URL, or the initiate step never encoded state into the authorize URL.

Common situations: Browser truncating/mangling the query string; proxies stripping query params; users re-opening an old callback link from history; broken authorize URL construction.

Related errors


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