Skip to content

REST: /api/media/file/* returns 404 on HEAD but 200 on GET (RFC 9110 §9.3.2 violation) #16754

@MarShao0124

Description

@MarShao0124

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

  1. Stand up any Payload 3.0+ site with the standard Media collection mounted under /api.
  2. Upload any file via the admin UI (e.g. portrait.jpg).
  3. 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):

  1. Export an explicit REST_HEAD from @payloadcms/next/routes that wires HEAD to the same handler chain as GET.
  2. Loosen the internal method check inside REST_GET to accept HEAD; Next.js strips the body automatically.
  3. Re-export HEAD = GET from @payloadcms/next/routes so consumers pick it up automatically.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions