{"record":{"id":"a9feb7e5a5806016","repo":"Budibase/budibase","slug":"slack-app-configuration-refresh-token-is-required","errorCode":null,"errorMessage":"Slack app configuration refresh token is required","messagePattern":"Slack app configuration refresh token is required","errorType":"http","errorClass":"HTTPError","httpStatus":400,"severity":"error","filePath":"packages/server/src/sdk/workspace/ai/slackAppConfig.ts","lineNumber":118,"sourceCode":"  }\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({\n    refreshToken:\n      trimmedRefreshToken === PASSWORD_REPLACEMENT && existing?.refreshToken\n        ? decodeSecret(existing.refreshToken)","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/sdk/workspace/ai/slackAppConfig.ts#L100-L136","documentation":"save() in slackAppConfig.ts also requires a non-empty refresh token, since tokens are rotated automatically via rotateSlackConfigToken. When the trimmed refreshToken is empty it throws this 400 HTTPError before contacting Slack or writing to the DB.","triggerScenarios":"Calling save(configToken, refreshToken) with refreshToken being \"\" or whitespace-only.","commonSituations":"Only pasted the config token and left the refresh token field empty; UI forgot to render/send the refresh token input; stored secret got wiped during migration.","solutions":["Supply the Slack app refresh token alongside the config token and retry.","Verify the client actually sends the refreshToken field (not undefined) in the payload.","If rotating an existing config, use the PASSWORD_REPLACEMENT sentinel instead of an empty string so the stored refresh token is reused."],"exampleFix":"// before\nawait sdk.ai.slackAppConfig.save(configToken, \"\")\n// after\nawait sdk.ai.slackAppConfig.save(configToken, refreshToken)","handlingStrategy":"validation","validationCode":"if (!refreshToken || !refreshToken.trim()) throw new Error(\"refreshToken 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    // prompt user for the refresh token\n  }\n  throw e\n}","preventionTips":["Always collect both config token and refresh token as a pair from Slack","Validate both fields client-side before submit","Use PASSWORD_REPLACEMENT only when a stored config exists"],"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"}