feat: automatically match GET routes for HEAD requests#1452
Conversation
HEAD requests now fall back to the matching GET route when no explicit HEAD (or all()) route is registered, following RFC 9110. The request method stays "HEAD", so the existing nullBody() logic strips the response body while the GET handler produces correct headers. Explicit head() routes still take precedence, and the raw-Response fast path now strips the body for HEAD requests for self-consistency. Closes #1451 Co-Authored-By: Claude Opus 4.8 <[email protected]>
📝 WalkthroughWalkthroughChangesHEAD request fallback
Estimated code review effort: 3 (Moderate) | ~25 minutes Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The H3 gzip size was unchanged and did not need the bump. Co-Authored-By: Claude Opus 4.8 <[email protected]>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/response.ts (1)
129-131: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winHEAD body not stripped on the mutable-headers merge path.
When
preparedHeadersexist, the response is OK, andmergeHeaderssucceeds (mutable headers),return valat line 131 returns the raw Response with its body intact for HEAD requests. StandardResponseobjects have immutable headers so thecatchpath (which callsnullBody) handles them, but custom Response implementations with mutable headers would slip through. This is a gap in the PR objective to strip bodies from all raw Response objects on HEAD.🛡️ Proposed fix: strip body for HEAD before returning merged response
try { mergeHeaders(val.headers, preparedHeaders, val.headers); + if (event.req.method === "HEAD" && val.body !== null) { + return new FastResponse(null, { + status: val.status, + statusText: val.statusText, + headers: val.headers, + }) as Response; + } return val; // Fast path: no headers to merge } catch {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/response.ts` around lines 129 - 131, In the mutable-header merge path within the response handling try block, ensure HEAD responses have their body stripped before returning. After mergeHeaders succeeds, call the existing nullBody helper (or equivalent HEAD body-removal logic) on val before return val, while preserving the current behavior for non-HEAD requests and the catch path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/response.ts`:
- Around line 129-131: In the mutable-header merge path within the response
handling try block, ensure HEAD responses have their body stripped before
returning. After mergeHeaders succeeds, call the existing nullBody helper (or
equivalent HEAD body-removal logic) on val before return val, while preserving
the current behavior for non-HEAD requests and the catch path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 53d646b8-fa3f-4c15-9720-d79aef323739
📒 Files selected for processing (5)
docs/1.guide/1.basics/2.routing.mdsrc/h3.tssrc/response.tstest/bench/bundle.test.tstest/router.test.ts
Guards against dropping a legitimately-set Content-Length on the HEAD fallback, for both event.res.headers and raw Response cases (RFC 9110: HEAD Content-Length should equal what GET would send). Co-Authored-By: Claude Opus 4.8 <[email protected]>
Method-scoped global middleware registered with { method: "GET" } now
also runs for HEAD requests, mirroring the routing fallback (HEAD is
served by GET handlers per RFC 9110). This matches Express, Hono, and
Elysia, which all run GET-scoped middleware for HEAD.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Relies on the HEAD->GET route fallback (#1452): with the `get` option, plain .get()/.query() registration now also covers HEAD requests. Co-Authored-By: Claude Fable 5 <[email protected]>
Relies on the HEAD->GET route fallback (#1452): with the `get` option, plain .get()/.query() registration now also covers HEAD requests. Co-Authored-By: Claude Fable 5 <[email protected]>
Summary
Implements #1451.
HEADrequests now automatically fall back to the matchingGETroute when no explicitHEAD(orall()) route is registered, following RFC 9110. This matches Express (implicit) and Fastify (exposeHeadRoutes: true) semantics, so users no longer hit surprise 404s from load balancer health checks, link checkers, or CDNs probing resources.The request method is not rewritten —
event.req.methodstays"HEAD", so the existingnullBody()logic inprepareResponsestrips the body while the GET handler produces correct headers.Changes
src/h3.ts—~findRoutefalls back to theGETroute on aHEADmiss. Zero cost on non-HEAD traffic (the extra lookup only runs on a HEAD route miss). Explicithead()routes still take precedence.src/response.ts— the raw-Responsefast path now strips the body forHEADrequests for self-consistency (runtimes discard HEAD bodies at the server layer anyway, but this keeps h3 internally consistent).src/middleware.ts— GET-method-scoped global middleware (app.use(fn, { method: "GET" })) now also runs forHEADrequests, mirroring the routing fallback.test/router.test.ts— newHEAD fallbackblock (viadescribeMatrix, runs web + node): GET fallback with empty body & preserved headers, explicithead()precedence, 404 when no GET route, raw-Responsebody stripping,all()route, route-level middleware, andcontent-lengthpreservation.test/middleware.test.ts— GET-scoped global middleware runs for HEAD.docs/1.guide/1.basics/2.routing.md— new "HEAD Requests" section.test/bench/bundle.test.ts— bumped bundle-size thresholds to accommodate the feature.Ecosystem alignment
Validated the behavior against three other frameworks (live repro + source inspection). All confirm both design decisions in this PR:
head()route takes precedenceContent-Lengthpreserved for HEADh3 matches the Express/Elysia model (fallback only on a HEAD miss, so explicit
head()remains a usable escape hatch), rather than Hono's rewrite-before-routing model where an explicithead()route is unreachable.Trade-offs (accepted, per the issue)
head()registration remains the optimization path.Test plan
pnpm vitest run test/router.test.ts test/middleware.test.ts— pass; confirmed the new tests fail before their respective fixes.pnpm test— full suite green (1341 passed, 23 skipped, no type errors).Closes #1451
🤖 Generated with Claude Code
Summary by CodeRabbit
HEADsupport by falling back to matchingGETroutes while omitting the response body.HEADhandlers override theGETfallback.HEADrequests now return404.HEAD, includingcontent-length(even whenGETreturns a raw response).GET-scoped middleware now also applies toHEAD.HEADfallback coverage, including middleware andall()routes.