Budibase/budibase · error · BadRequestError

SMTP password is required

Error message

SMTP password is required

What it means

processSMTPConfig supports a PASSWORD_REPLACEMENT placeholder that signals 'keep the existing stored password'. If the placeholder is sent but no existing config with a stored password exists, there is no password to retain, so a BadRequestError is thrown before email.verifyConfig runs.

Source

Thrown at packages/worker/src/api/controllers/global/configs.ts:180

async function hasActivatedConfig(ssoConfigs?: SSOConfigs) {
  if (!ssoConfigs) {
    ssoConfigs = await getSSOConfigs()
  }
  return !!Object.values(ssoConfigs).find(c => c?.activated)
}

async function processSMTPConfig(
  config: SMTPInnerConfig,
  existingConfig?: SMTPInnerConfig
) {
  if (config.auth?.pass === PASSWORD_REPLACEMENT) {
    // if the password is being replaced, use the existing password
    if (existingConfig && existingConfig.auth?.pass) {
      config.auth.pass = existingConfig.auth.pass
    } else {
      // otherwise, throw an error
      throw new BadRequestError("SMTP password is required")
    }
  }
  await email.verifyConfig(config)
}

async function processSettingsConfig(
  config: SettingsInnerConfig & SettingsBrandingConfig,
  existingConfig?: SettingsInnerConfig & SettingsBrandingConfig
) {
  if (config.isSSOEnforced) {
    const valid = await hasActivatedConfig()
    if (!valid) {
      throw new Error("Cannot enforce SSO without an activated configuration")
    }
  }

  // always preserve file attributes
  // these should be set via upload instead

View on GitHub (pinned to a81a902e9a)

Solutions

  1. Send the actual SMTP password in auth.pass instead of the placeholder when no password is stored yet
  2. Complete initial SMTP setup once with a real password; subsequent updates can use the placeholder
  3. Verify the existing config was saved with auth.pass populated before sending PASSWORD_REPLACEMENT
  4. Check the existingConfig lookup (tenant/type) is correct so the stored password is found

Example fix

// before
{ "auth": { "user": "smtp@example.com", "pass": "__REPLACEMENT__" } }
// after
{ "auth": { "user": "smtp@example.com", "pass": "<real-smtp-password>" } }
Defensive patterns

Strategy: validation

Validate before calling

const isReplacement = cfg.auth?.pass === PASSWORD_REPLACEMENT
const stored = existingConfig?.auth?.pass
if (isReplacement && !stored) {
  // prompt user for a real password before calling save
}

Type guard

function hasSmtpPassword(c: { auth?: { pass?: string } } | undefined): c is { auth: { pass: string } } {
  return typeof c?.auth?.pass === "string" && c.auth.pass.length > 0 && c.auth.pass !== PASSWORD_REPLACEMENT
}

Try / catch

try {
  await configApi.save(smtpConfig)
} catch (err) {
  if (err.status === 400 && err.message.includes("SMTP password is required")) {
    // re-open form and require the password field
  } else { throw err }
}

Prevention

When it happens

Trigger: Saving an SMTP config via the configs API where config.auth.pass equals the PASSWORD_REPLACEMENT marker while existingConfig is missing or existingConfig.auth.pass is empty.

Common situations: UI edit forms that always submit the placeholder to avoid echoing secrets, but the SMTP config was never fully saved before, or an API client copies an update payload from another tenant where the password was never set.

Related errors


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