payloadcms/payload · error · APIError

Missing ${collectionConfig.auth.loginWithUsername ? 'usernam

Error message

Missing ${collectionConfig.auth.loginWithUsername ? 'username' : 'email'}.

What it means

Thrown in `unlock` when both `sanitizedEmail` and `sanitizedUsername` are falsy after normalization. The operation needs an identifier to find the account to unlock. Message is dynamic: `'Missing username.'` when `loginWithUsername` is enabled, else `'Missing email.'`. `APIError` with HTTP 400 (BAD_REQUEST).

Source

Thrown at packages/payload/src/auth/operations/unlock.ts:55

  } = args

  const loginWithUsername = collectionConfig.auth.loginWithUsername

  const { canLoginWithEmail, canLoginWithUsername } = getLoginOptions(loginWithUsername)

  const sanitizedEmail = canLoginWithEmail && (args.data?.email || '').toLowerCase().trim()
  const sanitizedUsername =
    (canLoginWithUsername &&
      'username' in args.data &&
      typeof args.data.username === 'string' &&
      args.data.username.toLowerCase().trim()) ||
    null

  if (collectionConfig.auth.disableLocalStrategy) {
    throw new Forbidden(req.t)
  }
  if (!sanitizedEmail && !sanitizedUsername) {
    throw new APIError(
      `Missing ${collectionConfig.auth.loginWithUsername ? 'username' : 'email'}.`,
      httpStatus.BAD_REQUEST,
    )
  }

  try {
    args = await buildBeforeOperation({
      args,
      collection: args.collection.config,
      operation: 'unlock',
      overrideAccess,
    })

    const shouldCommit = await initTransaction(req)
    let whereConstraint: Where = {}

    // /////////////////////////////////////
    // Access

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Send the identifier your collection expects: `email` for default, or `username`/`email` when `loginWithUsername` is enabled.
  2. Validate non-empty (after trim) on the client before posting.
  3. Match the request shape to `auth.loginWithUsername` settings (`allowEmailLogin`, `canLoginWithUsername`).

Example fix

// before
await payload.unlock({ collection, data: { email: '  ' }, req })
// after
const email = rawEmail.trim()
if (email) {
  await payload.unlock({ collection, data: { email }, req })
}
Defensive patterns

Strategy: validation

Validate before calling

// Ensure an identifier is present and trimmed
const email = data.email?.trim()
const username = data.username?.trim()
if (!email && !username) {
  throw new Error('Email or username is required')
}
await payload.unlock({ collection, data: { ...(email ? { email } : { username }) }, req })

Type guard

function hasUnlockIdentifier(data: unknown): boolean {
  if (typeof data !== 'object' || !data) return false
  const d = data as Record<string, unknown>
  return typeof d.email === 'string' && d.email.trim() !== ''
    || typeof d.username === 'string' && (d.username as string).trim() !== ''
}

Try / catch

try {
  await payload.unlock({ collection, data, req })
} catch (e) {
  if (e instanceof APIError && e.status === 400 && /Missing/.test(e.message)) {
    // prompt user for email/username
  } else throw e
}

Prevention

When it happens

Trigger: A `POST /api/<collection>/unlock` request body omits both `email` and `username`; `loginWithUsername` is on with `allowEmailLogin` but the client sent neither; the field is present but whitespace-only (trimmed to empty); `username` present but `canLoginWithUsername` is false so it is ignored.

Common situations: Form posts an empty identifier field; `loginWithUsername` config added but the frontend still only sends one of the allowed identifiers; the identifier key is mistyped (`user` instead of `username`).

Related errors


AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12). Data as JSON: /api/errors/7de1265939ff4f3e. Report an issue: GitHub.