Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Vercel Blob Upload Issues since 3.90.X

Open
#18,362 2 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
nextjs, nodejs, playwright, typescript

Research direction

Start with the focused reproduction commands in test/_community and inspect getFileFromClientUpload.ts, generateFileData.ts, getIncomingFiles.ts, adapter.ts, uploadFile.ts, getClientUploadRoute.ts, and authorizeFileOverwrite.ts. Use the named header, WebP, and duplicate-filename cases first; done requires the focused assertions to pass, with the supplied browser checks and real Blob runtime still requiring separate verification.

Written by the indexing model from the issue text.

Description

Bug plugin: cloud-storage plugin: storage-* status: needs-triage v3
Describe the Bug

Three client-upload regressions: header cancellation hang, empty WebP replacement, and filename overwrite authorisation

This has been prepared with the help of codex, I hope that is okay

Summary

This single reproduction covers three independent failures in the Vercel Blob client-upload flow. It uses one minimal media collection (upload: true), the Vercel Blob adapter with clientUploads: true, and synthetic fixtures. No custom workaround plugins are installed.

Link to the code that reproduces this issue

https://github.com/goshawkup/payload-upload-issue-repro

Reproduction Steps

Reproduction steps

  1. Check out the linked reproduction branch (publication pending), based on Payload v3.90.2. Run these commands from the repository root:

    pnpm install --frozen-lockfile
    node test/_community/generate-fixtures.mjs
    pnpm test:int _community
    
  2. Observe five failing assertions across the three scenarios and four passing controls. These focused tests require no credentials or database. To isolate a scenario, append -t 'header-probe', -t 'WebP processed', or -t 'duplicate-filename' to the test command.

  3. For actual Admin uploads, set BLOB_READ_WRITE_TOKEN to a disposable test store's token in an ignored root .env, together with PAYLOAD_DATABASE=sqlite and SQLITE_URL=file:./community-repro.db. Run PAYLOAD_DATABASE=sqlite pnpm dev _community, open /admin, and log in with the upstream test credentials [email protected] / test.

  4. Timeout: create and save separate media documents with test/_community/fixtures/below.png, at.png, and above.png (one byte below, exactly at, and one byte above 1 MiB). Observe the initial Blob upload separately from finalisation via POST /api/media. Check that finalisation completes and each final URL returns a readable image. If the hang depends on the hosted runtime, repeat in a dedicated protected Vercel test deployment; local success alone does not rule it out.

  5. WebP: create and save separate documents using static.webp and animated.webp; upload blue.jpg and small.png as controls. Download each document's final URL. Check for non-empty, decodable bytes, 64×64 dimensions, and two frames for the animated WebP.

  6. Filename collision: upload blue and red JPEG contents as separate documents using the same requested filename, then fetch both final URLs and verify that each retains its own contents. Repeat against an existing server-uploaded/legacy object, which the supplied browser test seeds through the multipart API. Also test concurrent creation and intentional replacement separately. The automated tests assign matching filenames without modifying the fixtures.

  7. To run the prepared browser checks against the running disposable server:

    pnpm exec playwright install chromium
    REPRO_LIVE=1 REPRO_BASE_URL=http://localhost:3000 pnpm exec playwright test --config test/_community/browser.config.ts
    

The 11 browser checks are prepared but have not been executed against a real Blob store. The observed failures below are from the focused upstream-function tests, not a claim that live verification has been completed.

Environment and verification status

  • Payload v3.90.0 (e10b60228b92b680663110ea2af431643bfc6126)
  • Payload v3.90.2 (6254c3bf561a205223849a6497d7dae824d4e81b)
  • v3 maintenance head inspected/tested: 9106bf8c189b584d81016188579bcfa10db1e000
  • Node 24.12.0, pnpm 11.9.0, monorepo-root Sharp 0.35.4, Vitest 4.1.2
  • All three refs: 5 failed and 4 passed assertions in the focused reproduction suite.
  • These tests call upstream implementations with external SDK calls/database queries stubbed. Browser tests are supplied but real Blob and hosted-runtime execution remain outstanding. The deterministic cancellation test models a non-settling stream; it does not claim every Vercel runtime produces one.

Run pnpm install --frozen-lockfile, node test/_community/generate-fixtures.mjs, then pnpm test:int _community. The README documents actual Admin reproduction steps and optional live tests against a disposable store.

1. Header probe waits indefinitely for cancellation

Steps: upload valid PNGs below, at, and above the 1 MiB probe boundary. The deterministic test supplies the same bounded response shape but holds the underlying cancellation promise pending until test cleanup.

Expected: finalisation completes once the bounded header has been read; stream cleanup must not indefinitely block document creation.

Observed locally: the below-limit control finishes; at/above-limit assertions time out waiting for completion. fetchHeaderOnly awaits reader.cancel() in its finally block before returning the probed file.

Code: getFileFromClientUpload.ts at v3.90.2.

Runtime follow-up: reproduce with the real Blob streamed HTTP 206 response in a dedicated test deployment. Record status, byte count and elapsed time without exposing tokens, receipts or raw HAR files.

Application workaround (excluded from the reproduction): wrap the storage handler only for client-upload header probes requesting bytes=0-1048575 and receiving HTTP 206 with a valid Content-Length no greater than 1 MiB. Copy the bounded bytes into an owned buffer and return a memory-backed Response, preserving status and headers. Release the original reader without calling or awaiting its cancellation; Payload then cancels the memory-backed stream. Other reads remain streamed.

2. WebP processing hands an empty buffer to Blob

Steps: independently upload the generated static and two-frame animated WebP files; download/decode the final stored object. JPEG and PNG are controls.

Expected: non-empty, decodable WebP contents, 64×64 dimensions and preserved animation frames.

Observed locally: getFileFromClientUpload creates a temporary file and an empty req.file.data. generateFileData processes WebP and removes clientUploadContext, writing the result to the temporary file. getIncomingFiles forwards both the empty buffer and tempFilePath, but the Vercel Blob adapter consumes only the buffer. Its SDK put call therefore receives zero bytes with overwrite enabled. Both WebP assertions fail; JPEG/PNG controls pass.

Code: generateFileData.ts, getIncomingFiles.ts, adapter.ts, uploadFile.ts.

The test intercepts the final Blob SDK call; it does not mutate an actual remote object or run the complete database/hook lifecycle.

Application workaround (excluded from the reproduction): prepend an afterChange hook before the cloud-storage hook. For image/webp only, when tempFilePath matches req.context.payloadClientUploadTempFilePath, the client-upload marker is gone, and req.file.data is empty, read the processed temporary file into req.file.data and update req.file.size. Reject an empty temporary file rather than uploading zero bytes. This buffers the processed WebP in memory.

3. New-upload authorisation can overwrite an existing filename

Steps: create a server-uploaded/legacy photo.jpg, then start a new client-uploaded document using the same filename but different image contents. Also test fresh client uploads sequentially/concurrently and intentional replacement separately.

Expected: creating a new document preserves the existing object's bytes and returns a distinct usable object path. Intentional replacement updates the selected document and preserves its identity.

Observed locally: the actual issue-client-upload route authorises allowOverwrite: true and returns the existing photo.jpg path when its owner is update-accessible. The request has no target document ID and does not distinguish creation from intentional replacement. That path bypasses resolveSignedURLKey and its filename deduplication. The legacy overwrite assertion fails. Concurrent authorisations when no file exists get distinct object keys and pass the control assertion.

Code: getClientUploadRoute.ts, authorizeFileOverwrite.ts.

Existing report: https://github.com/payloadcms/payload/issues/17823 describes the resulting overwrite/404 behaviour. https://github.com/payloadcms/payload/pull/17849 was closed without merging. This section links that known issue rather than treating it as a new discovery. The supplied live tests check the complete persistence/URL behaviour, which is not claimed from the focused authorisation test alone.

Application workaround (excluded from the reproduction): wrap authenticated issue-client-upload=1 requests to the Blob upload route. Before delegating to Payload, sanitise the requested filename with getSanitizedUploadFilename, resolve an available name with getSafeFileName, and substitute that filename in the initialisation body. Payload signs and returns the resolved name before the browser uploads the bytes. This is not an atomic filename reservation; concurrency and intentional replacement still need independent validation.

Which area(s) are affected?

plugin: cloud-storage, plugin: storage-*

Environment Info
## Environment info — actual `pnpm payload info` output

Captured on 28 September 2026 from the affected application, which still uses Payload 3.90.0. The package-manager banner containing the private project name/path and unrelated npm configuration warnings are omitted; the diagnostic output below is otherwise unchanged. This describes the application environment, not the v3.90.2 reproduction checkout or a hosted Vercel runtime.


Binaries:
  Node: 24.12.0
  npm: 11.6.2
  Yarn: N/A
  pnpm: 10.25.0
Relevant Packages:
  payload: 3.90.0
  next: 16.3.3
  @payloadcms/db-postgres: 3.90.0
  @payloadcms/drizzle: 3.90.0
  @payloadcms/email-nodemailer: 3.90.0
  @payloadcms/graphql: 3.90.0
  @payloadcms/next/utilities: 3.90.0
  @payloadcms/payload-cloud: 3.90.0
  @payloadcms/plugin-cloud-storage: 3.90.0
  @payloadcms/plugin-search: 3.90.0
  @payloadcms/plugin-seo: 3.90.0
  @payloadcms/richtext-lexical: 3.90.0
  @payloadcms/storage-s3: 3.90.0
  @payloadcms/storage-vercel-blob: 3.90.0
  @payloadcms/translations: 3.90.0
  @payloadcms/ui/shared: 3.90.0
  react: 19.2.6
  react-dom: 19.2.6
Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 24.5.0: Tue Apr 22 19:54:43 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T8132
  Available memory (MB): 24576
  Available CPU cores: 10
Dominant language
TypeScript
Stars
45.5k
Forks
4.3k
Avg merge
1d 19h
Merged PRs (30d)
299

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from payloadcms/payload

All issues in payloadcms/payload

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.