payloadcms/payload · error · Forbidden

You are not allowed to perform this action.

Error message

You are not allowed to perform this action.

What it means

The staged-upload path (used when no storage adapter provides `uploadInstructions`) writes the file to Payload-local disk *before* a document is created or updated. Because that write happens outside a document transaction, Payload requires an authenticated `req.user`. If `req.user` is absent on the `POST /upload-instructions` request, a `Forbidden` (HTTP 403) is thrown. This applies only when the collection has no adapter `uploadInstructions` and `overrideAccess` is not set.

Source

Thrown at packages/payload/src/uploads/endpoints/uploadInstructions.ts:72

  }

  await checkFileRestrictions({
    checkFileContents: false,
    collection: collection.config,
    file: {
      name: upload.filename,
      data: Buffer.alloc(0),
      mimetype: upload.mimeType,
      size: upload.filesize,
    },
    req,
  })

  if (!uploadInstructions && !overrideAccess) {
    // Staged uploads write to Payload before a document is saved. Require a signed-in user who
    // can create or update documents in this collection.
    if (!req.user) {
      throw new Forbidden(req.t)
    }

    const collectionPermissions = (await getAccessResults({ req })).collections?.[
      upload.collectionSlug
    ]

    if (!collectionPermissions?.create && !collectionPermissions?.update) {
      throw new Forbidden(req.t)
    }
  }

  return uploadInstructions
    ? uploadInstructions.generate({ ...upload, overrideAccess, req })
    : generateStagedUploadInstructions({ ...upload, req })
}

export const uploadInstructionsEndpoint: Endpoint = {
  handler: async (req) => {

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Authenticate the request: send the Payload session cookie (`payload=<prefix>`), or call the endpoint from a context that runs `payload.login`/`payload.create` with a strategy token first.
  2. If this is a trusted server-side flow, call `getUploadInstructions` (or `payload.create`/`update`) with `overrideAccess: true` from the Local API.
  3. Re-add the storage adapter if uploads were meant to go straight to S3/R2/etc., which removes the staged-upload auth requirement.
  4. Check middleware/proxies that may be dropping `Cookie`/`Authorization` headers.

Example fix

// before — no auth on the client
await fetch(`${serverURL}/api/upload-instructions`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ collectionSlug, filename, filesize, mimeType }),
})

// after — include session cookie / token
await fetch(`${serverURL}/api/upload-instructions`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Cookie: `payload=${token}` },
  body: JSON.stringify({ collectionSlug, filename, filesize, mimeType }),
})
Defensive patterns

Strategy: validation

Validate before calling

// Ensure req.user is populated before calling the staged path
import { getPayload } from 'payload'
import config from './payload.config'

const payload = await getPayload({ config })

async function getInstructionsAsUser(user: { email: string; password: string }) {
  const { token } = await payload.login({ collection: 'users', data: user })
  const req = await payload.request({ collection: 'media' }) // or build a req with req.user
  // attach the user to the request before calling getUploadInstructions
  return payload.getUploadInstructions
    ? null
    : null
}
// Simpler: hit the REST endpoint with the cookie
async function postInstructions(token: string, body: object) {
  return fetch(`${serverURL}/api/upload-instructions`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Cookie: `payload=${token}` },
    body: JSON.stringify(body),
  })
}

Type guard

function hasUser(req: { user?: unknown }): req is { user: Record<string, unknown> } {
  return !!req.user && typeof req.user === 'object'
}

if (!hasUser(req)) {
  // authenticate or use overrideAccess: true on the Local API
}

Try / catch

const res = await fetch(`${url}/api/upload-instructions`, { method: 'POST', headers, body })
if (res.status === 403) {
  const { message } = await res.json().catch(() => ({}))
  if (/not allowed/i.test(message ?? '')) {
    // no session — prompt login / refresh token, then retry
  }
}

Prevention

When it happens

Trigger: `POST /api/upload-instructions` reaches `getUploadInstructions` with `overrideAccess` falsy, the target collection has no `config.upload.uploadInstructions` (i.e. local staged uploads), and the request carries no authenticated session (`req.user` is undefined).

Common situations: A custom upload client (CLI, mobile app, external service) calls the endpoint without sending the Payload session cookie or a Bearer/strategy token. A reverse proxy strips auth headers/cookies. The route is hit during SSR or a cron job with no logged-in user. A storage adapter (S3, R2, Vercel Blob) was removed or not registered, silently flipping the collection from adapter uploads to staged uploads and now requiring auth it did not before.

Related errors


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