payloadcms/payload · error

Forbidden

Error message

Forbidden

What it means

A plain Error('Forbidden') thrown by the TanStack Start server adapter's forbidden() helper. TanStack Router has no dedicated forbidden primitive, so the adapter surfaces a generic error to terminate the request boundary when framework code calls adapter.forbidden() (e.g. access control denied during an admin page render).

Source

Thrown at packages/tanstack-start/src/utilities/serverAdapter.server.ts:93

  },

  redirect: (path: string) => {
    // TanStack Router requires throwing the redirect() result directly
    // eslint-disable-next-line @typescript-eslint/only-throw-error
    throw redirect({ to: path })
  },

  permanentRedirect: (path: string) => {
    // TanStack Router does not have a separate permanent redirect primitive;
    // fall back to a regular redirect so existing behavior is preserved.
    // eslint-disable-next-line @typescript-eslint/only-throw-error
    throw redirect({ to: path })
  },

  forbidden: () => {
    // TanStack Router does not have a dedicated forbidden() helper; surface
    // a generic error so the request boundary still terminates the request.
    throw new Error('Forbidden')
  },

  unauthorized: () => {
    // TanStack Router does not have a dedicated unauthorized() helper; surface
    // a generic error so the request boundary still terminates the request.
    throw new Error('Unauthorized')
  },

  setCookie: (name: string, value: string, options?: CookieOptions) => {
    setResponseHeader('Set-Cookie', serializeCookie(name, value, options))
  },
}

/**
 * Navigation requested during an admin page render, recorded by
 * `createPageRenderServerAdapter`. The admin-page server function reads this
 * after `renderServerComponent` resolves.
 */

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Review the access-control logic that triggered forbidden() and confirm the user should indeed be denied.
  2. Ensure the user is authenticated and has the required role before reaching the guarded route.
  3. If using TanStack Start, handle this generic error in your error boundary to render a proper 403 page.

Example fix

// before — generic error, no status
forbidden: () => {
  throw new Error('Forbidden')
},

// after — attach a status for the error boundary to map
import { Forbidden } from 'payload'
forbidden: () => {
  throw new Forbidden()
},
Defensive patterns

Strategy: try-catch

Validate before calling

// The adapter's forbidden() is called by framework access checks;
// prevent it by ensuring access returns true for authorized users.
function userHasAccess(user: { role?: string } | null, requiredRole: string): boolean {
  return Boolean(user && user.role === requiredRole)
}

if (!userHasAccess(req.user, 'admin')) {
  // handle before the adapter calls forbidden()
}

Type guard

function isAdapterForbidden(err: unknown): err is Error {
  return err instanceof Error && err.message === 'Forbidden'
}

Try / catch

try {
  await renderAdminPage()
} catch (err) {
  if (isAdapterForbidden(err)) {
    renderForbiddenPage()
    return
  }
  throw err
}

Prevention

When it happens

Trigger: Code in the admin page-render pipeline calls the adapter's forbidden() — typically because an access-control check returned false during a TanStack Start server render of an admin route.

Common situations: Access policy returns false for the current user on a TanStack Start admin route; the adapter is used before auth is fully resolved; a custom route guard calls forbidden() incorrectly.

Understand the failure class

Related errors


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