Describe the Bug
Per RFC 9110 §9.3.2, a HEAD response must be identical to GET except for the absence of the response body. The /api/media/file/<filename> route returns 404 on HEAD while returning 200 on GET for the same URL — same path, same query string, only the HTTP method differs.
End-users are unaffected since browsers always issue GET for <img> fetches. Operator scripts and upstream proxies that use HEAD as a cheap existence probe report false 404s and have to work around with curl -s -o /dev/null -w '%{http_code}' (GET status-only).
Reproduction Steps
- Stand up any Payload 3.0+ site with the standard Media collection mounted under
/api.
- Upload any file via the admin UI (e.g.
portrait.jpg).
- Request the file:
URL='https://your-site.example.com/api/media/file/portrait.jpg'
curl -sI "$URL" | head -n1
# Observed: HTTP/2 404
# Expected: HTTP/2 200
curl -s -o /dev/null -w '%{http_code}\n' "$URL"
# Observed: 200
# Expected: 200
Reproducible across multiple Payload 3.x deployments we have tested.
Which area(s) are affected? (Select all that apply)
area: core
Environment Info
| Package |
Version |
| payload |
^3.0.0 |
| @payloadcms/next |
^3.0.0 |
| @payloadcms/db-postgres |
^3.0.0 |
| next |
15.4.x |
| node |
20.x |
Route handler: app/(payload)/api/[...slug]/route.ts exporting only GET / POST / DELETE / PATCH / PUT / OPTIONS from @payloadcms/next/routes — no explicit HEAD export.
Likely cause (speculative, unconfirmed)
Next.js 15 App Router auto-synthesizes a HEAD handler from GET when no explicit HEAD export exists. If REST_GET(config) from @payloadcms/next/routes performs an internal method check on request.method === 'GET' when matching the Media /file/* subroute, the auto-synthesized HEAD path would not hit the same branch and falls through to the default 404.
Fix candidates (any one resolves):
- Export an explicit
REST_HEAD from @payloadcms/next/routes that wires HEAD to the same handler chain as GET.
- Loosen the internal method check inside
REST_GET to accept HEAD; Next.js strips the body automatically.
- Re-export
HEAD = GET from @payloadcms/next/routes so consumers pick it up automatically.
Describe the Bug
Per RFC 9110 §9.3.2, a HEAD response must be identical to GET except for the absence of the response body. The
/api/media/file/<filename>route returns 404 on HEAD while returning 200 on GET for the same URL — same path, same query string, only the HTTP method differs.End-users are unaffected since browsers always issue GET for
<img>fetches. Operator scripts and upstream proxies that use HEAD as a cheap existence probe report false 404s and have to work around withcurl -s -o /dev/null -w '%{http_code}'(GET status-only).Reproduction Steps
/api.portrait.jpg).Reproducible across multiple Payload 3.x deployments we have tested.
Which area(s) are affected? (Select all that apply)
area: core
Environment Info
Route handler:
app/(payload)/api/[...slug]/route.tsexporting onlyGET / POST / DELETE / PATCH / PUT / OPTIONSfrom@payloadcms/next/routes— no explicit HEAD export.Likely cause (speculative, unconfirmed)
Next.js 15 App Router auto-synthesizes a HEAD handler from
GETwhen no explicit HEAD export exists. IfREST_GET(config)from@payloadcms/next/routesperforms an internal method check onrequest.method === 'GET'when matching the Media/file/*subroute, the auto-synthesized HEAD path would not hit the same branch and falls through to the default 404.Fix candidates (any one resolves):
REST_HEADfrom@payloadcms/next/routesthat wires HEAD to the same handler chain as GET.REST_GETto accept HEAD; Next.js strips the body automatically.HEAD = GETfrom@payloadcms/next/routesso consumers pick it up automatically.