payloadcms/payload · error · UnauthorizedError

Unauthorized, you must be logged in to make this request.

Error message

Unauthorized, you must be logged in to make this request.

What it means

Thrown as UnauthorizedError (HTTP 401) by buildFormState's catch block when a downstream function threw a plain Error whose message is exactly 'Unauthorized'. buildFormState wraps form-state construction; if any inner step (e.g. a server function like renderTab) throws the generic 'Unauthorized' string, this wrapper promotes it to a proper 401 UnauthorizedError.

Source

Thrown at packages/ui/src/utilities/buildFormState.ts:81

> = async (args) => {
  const { req } = args

  try {
    await canAccessAdmin({ req })
    const res = await buildFormState(args)

    return res
  } catch (err) {
    req.payload.logger.error({ err, msg: `There was an error building form state` })

    if (err.message === 'Could not find field schema for given path') {
      return {
        message: err.message,
      }
    }

    if (err.message === 'Unauthorized') {
      throw new UnauthorizedError()
    }

    return formatErrors(err)
  }
}

export const buildFormState = async (
  args: BuildFormStateArgs,
): Promise<BuildFormStateSuccessResult> => {
  const {
    id: idFromArgs,
    checkForStaleData,
    collectionSlug,
    data: incomingData,
    docPermissions,
    docPreferences,
    documentFormState,
    formState,

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Ensure the admin session is valid before the form-state build is triggered.
  2. Make sure nested server functions invoked during form-state build receive the authenticated req.user.
  3. Treat the 401 in the client as a signal to redirect to login and re-fetch form state.

Example fix

// before — inner function throws bare string
if (!req.user) throw new Error('Unauthorized')

// after — throw the typed error so callers can instanceof-check
import { UnauthorizedError } from 'payload'
if (!req.user) throw new UnauthorizedError()
// buildFormState's string-match catch becomes redundant and can be removed.
Defensive patterns

Strategy: try-catch

Validate before calling

function isAuthenticated(user: unknown): user is { id: string } {
  return Boolean(user)
}

if (!isAuthenticated(req.user)) {
  // avoid the nested 'Unauthorized' string throw entirely
  redirect('/login')
}

Type guard

import { UnauthorizedError } from 'payload'

function isUnauthorizedError(err: unknown): err is UnauthorizedError {
  return err instanceof UnauthorizedError
}

Try / catch

try {
  await buildFormState({ collectionSlug, schemaPath })
} catch (err) {
  if (isUnauthorizedError(err)) {
    redirectToLogin()
    return
  }
  throw err
}

Prevention

When it happens

Trigger: During form-state building, a nested operation throws new Error('Unauthorized') (the bare-string pattern from errors 391/393), and buildFormState's catch detects that exact message and re-throws UnauthorizedError.

Common situations: Session expired mid form-state build; a nested server function (renderTab/renderField) hit its !req.user guard; auth not propagated into a sub-call during form-state construction.

Understand the failure class

Related errors


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