File URLs get a trailing slash when Next.js trailingSlash is enabled (404 on every upload)

Open Beginner friendly
#19,292 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@guilhermemarketing is already working on this.

Since Nov 3, 2026.

  • #19293 by @guilhermemarketing — open

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
76/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
next.js, typescript
Domain
api, backend

Research direction

Start with packages/payload/src/uploads/generateFilePathOrURL.spec.ts, the reproduction linked in the issue, and trace how it calls formatAdminURL when NEXT_TRAILING_SLASH=true. Check the existing upload URL tests and verify that generated file URLs have no trailing slash, including URLs with a query string. Done when upload file URLs preserve the object path while admin and API routes can still use trailing slashes.

Written by the indexing model from the issue text.

Description

area: core area: uploads Bug
Describe the Bug

Since 3.90 (#18207), formatAdminURL appends a trailing slash to every URL it builds when Next.js runs with trailingSlash: true (NEXT_TRAILING_SLASH=true). generateFilePathOrURL builds upload URLs with it, so every file URL now ends with a slash:

url:          /api/media/file/image.png/
thumbnailURL: /api/media/file/image-300x300.png/

A file URL points at an object, not a page route. With the slash, storage adapters and CDNs resolve a different (missing) key, so every image rendered from doc.url returns 404. In our production site (Next.js on Cloudflare Workers, @payloadcms/storage-r2) every image broke right after upgrading from 3.89.0 to 3.92.1 and we had to roll back.

Admin and API routes are fine with the slash; only file URLs are affected. The same code is on main, where a ?original=true query would also end up as /image.png/?original=true.

Link to the code that reproduces this issue

https://github.com/guilhermemarketing/payload/blob/fix/file-url-trailing-slash/packages/payload/src/uploads/generateFilePathOrURL.spec.ts

Reproduction Steps
  1. Set trailingSlash: true in next.config.js (wrapped with withPayload).
  2. Upload an image to any upload collection.
  3. Read the document: url and every sizes.*.url end with /.
  4. Request that URL: 404 with a storage adapter (R2 in our case); with local storage the URL differs from the stored file path as well.

Unit-level reproduction:

process.env.NEXT_TRAILING_SLASH = 'true'
generateFilePathOrURL({ collectionSlug: 'media', config, filename: 'image.png', relative: true, urlOrPath: undefined })
// => '/api/media/file/image.png/'   (expected '/api/media/file/image.png')
Which area(s) are affected?

area: core, area: uploads

Environment Info
payload: 3.92.1 (also 3.90.x, 3.91.x; 3.89.0 is not affected)
@payloadcms/next: 3.92.1
@payloadcms/storage-r2: 3.92.1
next: 16.3.3
Node.js: 24.22.1
Database: @payloadcms/db-d1-sqlite 3.92.1
Dominant language
TypeScript
Stars
45.6k
Forks
4.4k
Avg merge
1d 23h
Merged PRs (30d)
237

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.