{"record":{"id":"2476a41f8fa999fd","repo":"Budibase/budibase","slug":"slack-app-configuration-token-is-required","errorCode":null,"errorMessage":"Slack app configuration token is required","messagePattern":"Slack app configuration token is required","errorType":"http","errorClass":"HTTPError","httpStatus":400,"severity":"error","filePath":"packages/server/src/sdk/workspace/ai/slackAppConfig.ts","lineNumber":115,"sourceCode":"      )\n    }\n    return decodeSecret(config.configToken)\n  }\n\n  const rotated = await rotateSlackConfigToken({\n    refreshToken: decodeSecret(config.refreshToken),\n  })\n  return decodeSecret((await saveRotatedConfig(config, rotated)).configToken)\n}\n\nexport const save = async (configToken: string, refreshToken: string) => {\n  const db = getWorkspaceDB()\n  const existing = await fetch()\n  const now = new Date().toISOString()\n  const trimmedToken = configToken.trim()\n  const trimmedRefreshToken = refreshToken.trim()\n  if (!trimmedToken) {\n    throw new HTTPError(\"Slack app configuration token is required\", 400)\n  }\n  if (!trimmedRefreshToken) {\n    throw new HTTPError(\n      \"Slack app configuration refresh token is required\",\n      400\n    )\n  }\n  if (trimmedToken === PASSWORD_REPLACEMENT && !existing?.configToken) {\n    throw new HTTPError(\"Slack app configuration token is required\", 400)\n  }\n  if (trimmedRefreshToken === PASSWORD_REPLACEMENT && !existing?.refreshToken) {\n    throw new HTTPError(\n      \"Slack app configuration refresh token is required\",\n      400\n    )\n  }\n\n  const rotated = await rotateSlackConfigToken({","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/sdk/workspace/ai/slackAppConfig.ts#L97-L133","documentation":"Saving a Slack app configuration requires a non-empty config token; the save() function in slackAppConfig.ts trims both incoming tokens and throws this 400 HTTPError when the config token is empty/whitespace-only. This guard runs before any Slack token rotation or DB write, so an invalid config never reaches storage.","triggerScenarios":"Calling save(configToken, refreshToken) (exposed via the Slack app config API endpoint) with configToken being \"\" or whitespace-only (e.g. \"   \").","commonSituations":"Developer cleared the token field in the UI and submitted; form binding failed so the token arrived empty; env/config value was never set before calling the endpoint.","solutions":["Provide the actual Slack app configuration token (bot/user token from the Slack app config) in the request body and retry.","Check the client form/field binding so the token input is actually submitted with the request.","Trim-check the value client-side before calling the save endpoint and surface a validation error instead."],"exampleFix":"// before\nawait sdk.ai.slackAppConfig.save(\"\", refreshToken)\n// after\nawait sdk.ai.slackAppConfig.save(\"xoxb-config-token\", refreshToken)","handlingStrategy":"validation","validationCode":"if (!configToken || !configToken.trim()) throw new Error(\"configToken is required\")\nawait sdk.ai.slackAppConfig.save(configToken, refreshToken)","typeGuard":"const hasToken = (v: unknown): v is string => typeof v === \"string\" && v.trim().length > 0","tryCatchPattern":"try {\n  await sdk.ai.slackAppConfig.save(configToken, refreshToken)\n} catch (e) {\n  if (e instanceof HTTPError && e.status === 400) {\n    // surface \"config token required\" to the user / re-prompt for the token\n  }\n  throw e\n}","preventionTips":["Trim-check both tokens before calling save()","Never bind save() to a form that allows empty submission","Log the payload shape (without secrets) to confirm fields are sent"],"tags":["validation","slack","bad-request"],"backgroundTag":"missing-required-field","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}