{"record":{"id":"e9924de9615506b6","repo":"Budibase/budibase","slug":"slack-oauth-callback-is-missing-state","errorCode":null,"errorMessage":"Slack OAuth callback is missing state","messagePattern":"Slack OAuth callback is missing state","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/server/src/api/controllers/ai/agents.ts","lineNumber":573,"sourceCode":"  })\n\n  ctx.body = {\n    success: true,\n    appId,\n    messagingEndpointUrl: slackAppDeployment.messagingEndpointUrl,\n    oauthAuthorizeUrl: buildSlackInstallUrl({\n      oauthAuthorizeUrl,\n      redirectUri: oauthRedirectUrl,\n      state,\n    }),\n  }\n  ctx.status = 200\n}\n\nexport async function completeSlackOAuth(ctx: UserCtx<void, void>) {\n  const state = String(ctx.query.state || \"\").trim()\n  if (!state) {\n    throw new Error(\"Slack OAuth callback is missing state\")\n  }\n\n  const cacheKey = getSlackOAuthStateCacheKey(state)\n  const statePayload = (await cache.get(cacheKey, {\n    useTenancy: false,\n  })) as SlackOAuthState | undefined\n  await cache.destroy(cacheKey, { useTenancy: false })\n  if (!statePayload?.agentId || !statePayload.workspaceId) {\n    throw new Error(\"Slack OAuth state is invalid or expired\")\n  }\n\n  const oauthError = String(ctx.query.error || \"\").trim()\n  if (oauthError) {\n    throw new Error(\"Slack OAuth authorization failed\")\n  }\n\n  const code = String(ctx.query.code || \"\").trim()\n  if (!code) {","sourceCodeStart":555,"sourceCodeEnd":591,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/api/controllers/ai/agents.ts#L555-L591","documentation":"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.","triggerScenarios":"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.","commonSituations":"Browser truncating/mangling the query string; proxies stripping query params; users re-opening an old callback link from history; broken authorize URL construction.","solutions":["Restart the OAuth flow from the beginning (re-initiate so a fresh state is generated and embedded in the Slack authorize URL)","Check that the redirect/authorize URL construction includes state and that the registered Slack redirect URL preserves query params","Have the user use the original install link rather than a saved callback URL","Confirm no reverse proxy or middleware is stripping query parameters"],"exampleFix":"// before\nconst state = String(ctx.query.state || \"\").trim()\n// after (caller-side check before hitting the callback)\nif (!url.searchParams.get(\"state\")) {\n  url.searchParams.set(\"state\", generatedState)\n}","handlingStrategy":"validation","validationCode":"const state = new URL(callbackUrl).searchParams.get(\"state\")\nif (!state) {\n  // restart the flow instead of calling the callback handler\n  return initiateSlackOAuth(agentId)\n}","typeGuard":"function hasOAuthState(q: Record<string, unknown>): q is Record<string, string> & { state: string } {\n  return typeof q.state === \"string\" && q.state.trim() !== \"\"\n}","tryCatchPattern":"try {\n  await completeSlackOAuth(ctx)\n} catch (err) {\n  if (err.message.includes(\"missing state\")) {\n    // redirect the user to restart the Slack installation flow\n  } else { throw err }\n}","preventionTips":["Always build the Slack authorize URL with a state parameter","Never share or bookmark the callback URL; it is flow-internal","Ensure proxies preserve query strings on redirects","Surface a friendly 'restart install' page when state is missing"],"tags":["slack","oauth","csrf-state","callback"],"backgroundTag":"oauth-state-missing","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}