Skip to content

beforeValidate hooks break Media uploads in Cloudflare Workers deployment #14159

@heatloss

Description

@heatloss

Describe the Bug

Media file uploads fail with a 500 error in Cloudflare Workers (preview/production) when using beforeValidate or beforeChange hooks on upload-enabled collections. The same code works perfectly in local development.

Link to the code that reproduces this issue

https://github.com/heatloss/chimera-d1/tree/bug-test-1

Reproduction Steps

Reproduction Steps for Payload CMS Workers Hook Bug

Quick Reproduction (5 minutes)

1. Create from Official Template

npx create-payload-app@latest my-test-app

Select:

2. Add Custom ID Hook

Edit src/collections/Media.ts and add the custom ID field with hook:

import type { CollectionConfig } from 'payload'

export const Media: CollectionConfig = {
  slug: 'media',
  access: {
    read: () => true,
  },
  fields: [
    {
      name: 'id',
      type: 'text',
      required: true,
      admin: {
        hidden: true,
      },
      hooks: {
        beforeValidate: [
          ({ value, operation }) => {
            if (operation === 'create' && !value) {
              return crypto.randomUUID()
            }
            return value
          }
        ]
      }
    },
    {
      name: 'alt',
      type: 'text',
      required: true,
    },
  ],
  upload: {
    crop: false,
    focalPoint: false,
  },
}

3. Test Local Dev (Works)

pnpm dev

Navigate to http://localhost:3000/admin/collections/media and upload a file.
Result: ✅ Upload succeeds

4. Test Workers Preview (Fails)

pnpm wrangler login
pnpm preview

Navigate to the preview URL /admin/collections/media and upload a file.
Result: ❌ POST /api/media 500 Internal Server Error

Expected vs Actual

  • Expected: Hooks work in both dev and production
  • Actual: Hooks work in dev, fail with 500 error in Workers

Verification Test

To verify this is hook-related, remove the hooks section from the id field and change required: true to required: false:

{
  name: 'id',
  type: 'text',
  required: false,  // Changed
  admin: {
    hidden: true,
  },
  // hooks removed
}

Rebuild and test preview again:
Result: ✅ Upload succeeds (without custom ID generation)

Full Reproduction Repository

For a complete working example with all testing steps documented, see:
[Your GitHub repo URL here if you create one]

Or follow the steps above starting from the official Payload Cloudflare template.

Which area(s) are affected? (Select all that apply)

area: core, area: templates

Environment Info

Binaries:
    Node: 22.18.0
    npm: 10.9.3
    pnpm: 10.18.0

  Relevant Packages:
    payload: 3.58.0
    next: 15.5.4
    @payloadcms/graphql: 3.58.0
    @payloadcms/next/utilities: 3.58.0
    @payloadcms/plugin-cloud-storage: 3.58.0
    @payloadcms/richtext-lexical: 3.58.0
    @payloadcms/db-d1-sqlite: 3.58.0
    @payloadcms/storage-r2: 3.58.0
    @opennextjs/cloudflare: 1.10.1
    react: 19.1.0
    react-dom: 19.1.0
    wrangler: 4.42.2

  Operating System:
    Platform: darwin (macOS)
    Arch: arm64
    Node: 22.18.0

  Deployment Environment:
    - Local dev: Node.js (works ✅)
    - Production: Cloudflare Workers via OpenNext (fails ❌)
    - Database: D1 (remote mode)
    - Storage: R2

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: needs-triagePossible bug which hasn't been reproduced yet

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions