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 = {}
// /////////////////////////////////////
// AccessView on GitHub (pinned to 00c58b35c0)
Solutions
- Send the identifier your collection expects: `email` for default, or `username`/`email` when `loginWithUsername` is enabled.
- Validate non-empty (after trim) on the client before posting.
- 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
- Validate the identifier is non-empty (post-trim) before posting.
- Send the identifier matching `auth.loginWithUsername` settings.
- Reject whitespace-only submissions client-side.
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
- Missing required data.
- Missing required data.
- Username or email is required
- validation:required
- error:notAllowedToPerformAction
AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12).
Data as JSON: /api/errors/7de1265939ff4f3e.
Report an issue: GitHub.