Budibase/budibase · error · HTTPError

Slack app configuration token has expired. Save a new config

Error message

Slack app configuration token has expired. Save a new config token and refresh token.

What it means

fetchConfigToken tries to auto-rotate an expired (or near-expiry) config token using the stored refresh token. If the token needs rotation, there is no refreshToken, and an expiresAt is present (so the token is known-expired), rotation is impossible and this 400 HTTPError is thrown, instructing the user to re-enter both tokens.

Source

Thrown at packages/server/src/sdk/workspace/ai/slackAppConfig.ts:94

  }
}

export const fetchConfigToken = async () => {
  const config = await fetch()
  if (!config?.configToken) {
    throw new HTTPError(
      "Slack app configuration token is not configured for this workspace",
      400
    )
  }

  if (!tokenNeedsRotation(config.expiresAt) && config.expiresAt) {
    return decodeSecret(config.configToken)
  }

  if (!config.refreshToken) {
    if (config.expiresAt) {
      throw new HTTPError(
        "Slack app configuration token has expired. Save a new config token and refresh token.",
        400
      )
    }
    return decodeSecret(config.configToken)
  }

  const rotated = await rotateSlackConfigToken({
    refreshToken: decodeSecret(config.refreshToken),
  })
  return decodeSecret((await saveRotatedConfig(config, rotated)).configToken)
}

export const save = async (configToken: string, refreshToken: string) => {
  const db = getWorkspaceDB()
  const existing = await fetch()
  const now = new Date().toISOString()
  const trimmedToken = configToken.trim()

View on GitHub (pinned to a81a902e9a)

Solutions

  1. Re-save both a new config token and a new refresh token via the Slack app config save endpoint
  2. Generate fresh tokens from the Slack app configuration source and update the workspace config
  3. If a refresh token exists elsewhere, save it so auto-rotation can work
  4. Add validation at save time to require a refresh token so this state cannot occur

Example fix

// before
{ configToken: "bbai_enc::...", expiresAt: "2026-08-01T00:00:00Z" } // no refreshToken
// after
await save(newConfigToken, newRefreshToken)
// results in
{ configToken: "bbai_enc::...", refreshToken: "bbai_enc::...", expiresAt: "2026-09-01T00:00:00Z" }
Defensive patterns

Strategy: validation

Validate before calling

const config = await slackAppConfig.fetch()
const expired = config?.expiresAt && new Date(config.expiresAt).getTime() <= Date.now() + 5 * 60 * 1000
if (expired && !config?.refreshToken) {
  throw new Error("Config token expired and no refresh token saved; re-save both tokens")
}

Type guard

const isRefreshable = (
  config: SlackAppConfig | undefined
): config is SlackAppConfig & { refreshToken: string } =>
  typeof config?.refreshToken === "string" && config.refreshToken.length > 0

Try / catch

try {
  const token = await fetchConfigToken()
} catch (err) {
  if (err instanceof HTTPError && err.status === 400 && err.message.includes("has expired")) {
    // prompt user to save a new config token and refresh token
  } else { throw err }
}

Prevention

When it happens

Trigger: Config token's expiresAt is within the 5-minute rotation buffer or past, config.refreshToken is absent, and configToken itself is expired — the no-refresh-token + expired-token combination.

Common situations: User saved only a config token without a refresh token; refresh token was cleared/removed from the config; token aged past expiry while refresh token was lost; legacy config saved before refresh tokens were required.

Related errors


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