fix(server): answer HEAD like GET instead of 404 on published pages - #312
Open
ngocht wants to merge 1 commit into
Open
fix(server): answer HEAD like GET instead of 404 on published pages#312ngocht wants to merge 1 commit into
ngocht wants to merge 1 commit into
Conversation
RFC 9110 §9.3.2 defines HEAD as identical to GET except that the server
must not send content. Every route handler in the dispatcher table gated
on `req.method !== 'GET'`, so a HEAD matched none of them and fell all
the way through to the terminal JSON 404 — published pages answered GET
with 200 and HEAD with 404 {"error":"Not found"}. Uptime monitors and
link checkers probe with HEAD by convention, so a healthy site read as
permanently down and every internal link read as broken.
Normalise HEAD to GET once in `handleServerRequest` rather than patching
each method-gated handler: that makes the guarantee hold for every route,
including ones added later, and it also settles the third behaviour the
issue noted — /admin/api/cms/setup/status answered HEAD with 405 and now
answers 200 like GET. Only genuinely unsupported methods still get a 405.
Dropping the body stays Bun.serve's job; it already emits HEAD responses
with the headers a GET would have produced, content-length included, and
no content. The rewritten Request keeps the URL, cookies, and the
synthetic x-bun-socket-ip header stamped at the Bun.serve boundary, so
clientIp() attribution is unaffected.
With the invariant centralised, the two handlers that carried their own
`&& req.method !== 'HEAD'` clause no longer need it — removed so there is
one rule: handlers gate on GET, the dispatcher guarantees HEAD arrives
as GET.
Fixes CoreBunch#306
ngocht
marked this pull request as ready for review
July 30, 2026 09:28
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #306.
What changed
handleServerRequestnow normalisesHEADtoGETonce, before walking the route table.Every route handler gated on
req.method !== 'GET', so aHEADmatched none of them and fell through to the dispatcher's terminaljsonResponse({ error: 'Not found' }, { status: 404 }). Published pages answeredGETwith200andHEADwith404./healthand/adminwere unaffected only because those two handlers never check the method, which is what made the symptom look selective.RFC 9110 §9.3.2 defines
HEADas identical toGETexcept that the server must not send content, so the invariant belongs in the dispatcher rather than in each handler — that way it holds for routes added later too.Two behaviours this relies on, both verified before writing the fix:
Bun.servealready drops the body from aHEADresponse while preserving the headers aGETwould have produced,content-lengthincluded. Handlers stay body-agnostic.new Request(req, { method: 'GET' })preserves the URL, cookies, and the syntheticx-bun-socket-ipheader thatstampSocketIpstamps at theBun.serveboundary, soclientIp()attribution is unaffected.It also settles the third behaviour the issue noted:
/admin/api/cms/setup/statusansweredHEADwith405and now answers200likeGET. Methods a route genuinely doesn't support still get their405.With the invariant centralised, the two handlers that carried their own
&& req.method !== 'HEAD'clause (tryServeMediaRedirect,tryServeRuntimePackage) no longer need it. Removed, so there is one rule: handlers gate onGET, the dispatcher guarantees aHEADarrives as aGET.Impact
HEADnow see published pages as reachable. No configuration change, no migration.GETonly. Documented indocs/server.md.Verification
Tests were written first and observed failing with
404,404,405— reproducing the issue — before the fix.Wire-level check through a real
Bun.serveusing the actual dispatcher, mirroring the reproduction in the issue:/health/admin/api/cms/setup/status//kontakt/does-not-exist