payloadcms / payloadcms/payload
REST API returns HTTP 404 with a valid body when running on Next.js 16
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 44.8k
- Forks
- 4.2k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 53
Description
Link to reproduction
(I can prepare a minimum reproducible example if requested — let me know.)
Describe the Bug
When running Payload v3 on Next.js 16, all REST API routes handled by the auto-generated src/app/(payload)/api/[...slug]/route.ts (e.g. /api/users/, /api/media/file/<filename>) return HTTP 404 even when Payload itself processes the request successfully and returns a valid response body.
Key characteristics:
- The response body is correct (e.g.
/api/users/returns the proper{"docs": [...], "totalDocs": ..., ...}JSON,/api/media/file/foo.webpreturns the full image bytes with the rightcontent-length) - Only the HTTP status is wrong — it is
404instead of200 - Response headers include
X-Powered-By: Next.js, Payload, confirming Payload is in the chain - Response headers also include
vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, which is unusual for an API Route Handler - Custom Route Handlers defined directly under
src/app/api/<name>/route.ts(independent of the Payload catch-all) return200correctly - The issue reproduces on both production build (
next build && next startwithoutput: 'standalone') and development (pnpm devwith turbopack) - Behavior is independent of the request
Hostheader in the staging/dev environment; in production we observed that requests made via internal Docker network withHost: <container-name>:3000returned200, while any external hostname (cocorograph.co,dev.cocorograph.co, evenexample.com) returned404
Practical impact:
- Front-end image rendering through
/_next/imagestill works (Next.js's optimizer fetches the upstream, gets the bytes, and serves them anyway), so the issue is not visible in the browser - However, any script that calls the REST API directly (e.g.
curl -sI, OG image fetchers, indexing crawlers, internal automation that hits/api/<collection>/file/<filename>) sees a 404 and behaves as if the resource is missing — this caused a misdiagnosis in our content workflow
Reproduction Steps
- Set up a Payload v3.84.1 project with the default
npx create-payload-apptemplate - Use
next@^16.2.6,react@^19.2.6inpackage.json - Set
trailingSlash: trueandoutput: 'standalone'innext.config.mjs - Create a
Mediacollection with default upload config (local storage,staticDir: 'media'), upload a file via the admin panel - Build and start in production mode (or run
pnpm dev) curl -sI https://your-site/api/users/→ returns 404 even though body is the correct user list JSONcurl -sI https://your-site/api/media/file/<uploaded-filename>→ returns 404 even though body is the full image bytes
Which area(s) are affected? (Select all that apply)
area: core, area: ui, area: next-payload
Environment Info
Payload:
payload : 3.84.1
@payloadcms/db-postgres : 3.84.1
@payloadcms/email-nodemailer : 3.84.1
@payloadcms/live-preview-react : 3.84.1
@payloadcms/next : 3.84.1
@payloadcms/richtext-lexical : 3.84.1
@payloadcms/translations : 3.84.1
@payloadcms/ui : 3.84.1
Framework:
next : 16.2.6
react : 19.2.6
react-dom : 19.2.6
Runtime:
Node.js : 24.16.0
Operating System : Ubuntu 22.04 LTS (Docker container, node:24-bookworm-slim)
Database:
PostgreSQL : 18 (via @payloadcms/db-postgres)
Additional context
What we have tried (none of which fixed it):
- Upgrading from Payload 3.81 → 3.84.1 and Next.js 16.2.1 → 16.2.6 — confirmed both old and new versions exhibit the same behavior
- Bypassing our custom
proxy.ts(Next.js 16 middleware) for/api/paths by settingconfig.matcher: ['/((?!favicon\\.ico|api/).*)']— the proxy-set headers disappear from the response (so the matcher works), but the 404 status and thevary: rsc, ...headers remain - Hitting the container directly via the Docker network (
docker run --rm --network <net> curlimages/curl -sI http://app-container:3000/api/media/file/foo.webp) — returns 200 withHost: app-container:3000, but the same container returns 404 when any otherHostheader is sent - Confirmed
.next/server/app/(payload)/api/[...slug]/route.jsexists in the production build output
Hypotheses (unconfirmed):
- The
vary: rsc, next-router-state-tree, ...headers suggest Next.js 16 is routing the API response through the RSC processing pipeline, which may be overwriting the status code - The host-dependent behavior in production hints at some sort of internal cache key or self-host detection differing between in-cluster and external requests
Happy to provide a minimum reproducible repo or run further diagnostics if helpful. Thanks for the great work on Payload v3!
Contributor 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.
Research direction
Start with the generated src/app/(payload)/api/[...slug]/route.ts and compare it with a custom src/app/api//route.ts under Next.js 16. Review next.config.mjs and proxy.ts, then reproduce with the listed curl requests in development and production builds, varying the Host header. Done means Payload REST routes return HTTP 200 with their existing valid bodies and expected headers.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- next.js, typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100