File URLs get a trailing slash when Next.js trailingSlash is enabled (404 on every upload)
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
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
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
Reproduction Steps
- Set
trailingSlash: trueinnext.config.js(wrapped withwithPayload). - Upload an image to any upload collection.
- Read the document:
urland everysizes.*.urlend with/. - 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
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from payloadcms/payload
-
plugin-multi-tenant: `isGlobal` collections redirect tenant-scoped users to `/admin` since 3.93.0 (GlobalViewRedirect prop-name mismatch)Possibly taken A pull request linked to this issue is open or already merged. OpenBug plugin: multi-tenant
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
payloadcms/payload#19420 ·
Maintainers usually reply within 1 day
-
area: ui Bug created-by: Contributor status: needs-triage v3
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
payloadcms/payload#19413 ·
Maintainers usually reply within 1 day
-
area: ui Bug created-by: Contributor status: needs-triage v3
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
payloadcms/payload#19412 ·
Maintainers usually reply within 1 day
-
area: ui Bug created-by: Contributor status: needs-triage v3
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
payloadcms/payload#19403 ·
Maintainers usually reply within 1 day
-
area: ui Bug created-by: Contributor status: needs-triage v3
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
payloadcms/payload#19396 ·
Maintainers usually reply within 1 day
All issues in payloadcms/payload
Similar issues
-
twilio.webhook() rejects valid requests when the query string has an encoded `?` and a `+` spaceOpen
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
twilio/twilio-node#1232 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
Maintainers usually reply within 1 day
-
Tiling managers are not destroyed in disable(), so window-created handlers keep running (Gio.Settings constructed without schema)Possibly taken @gnacho claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
domferr/tilingshell#641 ·
Maintainers usually reply within 3 days
-
🐛 bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
LibreChat-AI/LibreChat#17887 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100