Skip to content

storage-vercel-blob: clientUploads makes the item unique after PUT so the new media link gets 404s and the existing file for non postfixed version is overwritten #17823

Open

Description

@zakriabilal

Describe the Bug

If you upload the same filename twice, Payload creates a new media item (photo-1.jpg), but that original file uploaded never gets stored. The admin thumbnail looks fine. The main URL 404s. And the first photo.jpg in Blob gets overwritten, even though this is meant to be a new file.

What’s happening: with clientUploads: true, the browser PUTs photo.jpg first (allowOverwrite: true). Payload then uniquifies the new row to photo-1.jpg and only writes the sizes under that name. So photo-1-300x….jpg exists, photo-1.jpg does not, and the old photo.jpg blob is replaced.

It shouldn’t overwrite the old blob, and the new -1 row should have a file behind it. Local disk / server-side uploads already do this.

addRandomSuffix is not a workaround — it breaks image sizes (#9589).

Image Image

Happy to open a 3.x PR that uniquifies the blob key before the client PUT

Link to the code that reproduces this issue

https://github.com/zakriabilal/payload-vercel-blob-filename-collision

Reproduction Steps

  1. You need your own Vercel Blob store and a BLOB_READ_WRITE_TOKEN. This does not reproduce on local disk.
  2. Clone https://github.com/zakriabilal/payload-vercel-blob-filename-collision
  3. Copy .env.example to .env. Set DATABASE_URL (local Mongo), PAYLOAD_SECRET, and BLOB_READ_WRITE_TOKEN.
  4. pnpm install && pnpm dev
  5. Create the first user at /admin
  6. Media → upload photo.jpg from the repo root
  7. Upload photo.jpg again as a new item (same filename)
  8. Library shows photo.jpg and photo-1.jpg. Both thumbnails look fine
  9. Open the original URL for photo-1.jpg — 404
  10. In the Vercel Blob store: photo-1-300x….jpg exists, photo-1.jpg does not, and the first photo.jpg was overwritten

Which area(s) are affected?

plugin: other

Environment Info

Binaries:
  Node: 24.14.0
  npm: 11.11.1
  Yarn: 1.22.22
  pnpm: 11.21.0
Relevant Packages:
  payload: 3.88.0
  next: 16.3.0
  @payloadcms/db-mongodb: 3.88.0
  @payloadcms/graphql: 3.88.0
  @payloadcms/next/utilities: 3.88.0
  @payloadcms/plugin-cloud-storage: 3.88.0
  @payloadcms/richtext-lexical: 3.88.0
  @payloadcms/storage-vercel-blob: 3.88.0
  @payloadcms/translations: 3.88.0
  @payloadcms/ui/shared: 3.88.0
  react: 19.2.6
  react-dom: 19.2.6
Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.5.0: Tue Jun  9 22:28:29 PDT 2026; root:xnu-12377.121.10~1/RELEASE_ARM64_T6030
  Available memory (MB): 18432
  Available CPU cores: 11

Activity

  1. added
    status: needs-triagePossible bug which hasn't been reproduced yet
    validate-reproductionAuto-added tag on create to tell bot to check recreation URL, removed after check.
    on Aug 17, 2026
  2. added and removed
    validate-reproductionAuto-added tag on create to tell bot to check recreation URL, removed after check.
    on Aug 17, 2026
  3. self-assigned this
    on Aug 17, 2026
  4. LukasHechenberger commented on Sep 25, 2026

    @LukasHechenberger

    Confirming this reproduces identically on payload@3.90.2 / @payloadcms/storage-vercel-blob@3.90.2 with Vercel Blob on Postgres (not just Mongo) — same symptom (404 on the new doc's file, old doc's blob overwritten).

    Root cause, traced through getClientUploadRoute.js / authorizeFileOverwrite.js: authorizeClientOverwrite() matches the raw, pre-dedup requested filename against existing docs' filename. On a match it returns allowOverwrite: true unconditionally — there's no signal in the handshake distinguishing "creating a new doc" from "intentionally replacing this doc's file". So the client PUTs to the old, un-keyed prefix/filename path, while generateFileData() independently dedupes the new doc's filename field to photo-1.jpg — a path that was never written. Confirmed via direct DB inspection: every doc with a filename collision has _objectKey: null; every doc with a unique filename gets a correct UUID _objectKey from resolveSignedURLKey().

    We patched around it locally (pnpm patch) by forcing allowOverwrite to always be false in getClientUploadRoute.js, so every client upload goes through resolveSignedURLKey() and gets its own unique, _objectKey-keyed path. That's a blunt fix (it disables the "replace an existing doc's file via client upload" feature, which we don't use) — @zakriabilal's suggestion of uniquifying the blob key before the client PUT is the better general fix, since it'd preserve intentional-overwrite support. Happy to help test a PR against this.

  5. franknoel commented on Sep 26, 2026

    @franknoel

    Adding to the 3.90.2 report above: the overwrite can also hit a different, existing image when an editor replaces the file on another document.

    On our staging site (storage-vercel-blob, clientUploads: true):

    • Image A had no _objectKey and was stored at staging/nature-pres-de-chez-vous.webp; it is used by published pages.
    • An editor replaced the file on image B with a file of the same name. authorizeClientOverwrite matched A, the only document at that prefix/filename, so the browser PUT replaced A's file.
    • On save, getSafeFileName renamed B to nature-pres-de-chez-vous-1.webp, a key the browser never uploaded to. Because it was a WebP, storage-vercel-blob: clientUploads stores WebP, GIF and cropped uploads as 0-byte objects #18333 then wrote a 0-byte object there.

    So one save overwrote an image already in use and left the edited one empty.

    The client-upload request only carries the collection, prefix, filename and MIME type, so the route cannot tell which document is being edited. The overwrite authorization also needs to know that document, and allow overwriting only its own key.

    #17824 targets the rename half, but it re-uploads file.data, which is empty for client uploads since #17856, so on Vercel Blob it would store 0 bytes (#18333).

    We switched to clientUploads: false until both are fixed.

  6. w3jerry commented on Nov 7, 2026

    @w3jerry

    Still reproduces on 3.93.0, and I can add one more case that loses data: replacing a doc's file with a file of the same name, on that same doc. The save returns HTTP 200 and the editor sees no error.

    Doc before Browser PUT goes to Doc after Result
    Has _objectKey, stored at <key>/file.pdf file.pdf <key>/file-1.pdf Doc URL 404. Old object deleted. New bytes orphaned at file.pdf.
    No _objectKey, stored at file.pdf file.pdf (overwrites it) file-1.pdf Doc URL 404. file.pdf is then deleted, so the old and the new file are both gone.

    A re-save can't recover either doc. The new-doc case from the original report also still fails on 3.93.0.

    Two details that may help with the fix:

    • getDocumentFileKeys in authorizeFileOverwrite.js builds keys from doc.prefix and ignores _objectKey. A keyed doc therefore "matches" prefix/filename, a path where its object isn't stored, so the upload is authorized to go there.
    • Nothing in payload/dist reads allowOverwrite. generateFileData still calls getSafeFileName, which matches the doc being replaced, so the rename happens even when overwriting the doc's own file. In the second row, the plugin's afterChange then sees different old and new keys and deletes the file that was just uploaded.

    The workaround from the first comment here (force allowOverwrite to false in getClientUploadRoute.js) fixed all three cases in the same tests on 3.90.2. On a real Vercel Blob store in production (3.90.2), a new doc with a filename already in use got a working file-1.pdf. Replacing a file with one whose name was already taken gave a working file-2.pdf and deleted the previous object. The target code in getClientUploadRoute.js is unchanged in 3.93.0, so the same patch should still apply.

    How I tested: the real admin UI in a browser against next build && next start, saving through the normal form. The adapter and plugin code ran for real, but the Vercel Blob API was a local stand-in, so the 3.93.0 results are not from a real store. Config: clientUploads: true, addRandomSuffix: true, disablePayloadAccessControl: true, on PDFs. I didn't test with addRandomSuffix: false, though these PDF uploads never received a suffix.

    payload, @payloadcms/storage-vercel-blob, @payloadcms/plugin-cloud-storage, @payloadcms/db-mongodb: 3.93.0
    next: 16.3.8
    node: 22.23.4
    mongodb: 7
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions