-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(start-server-core): fall back to GET handler for HEAD requests (RFC 9110 §9.3.2) #7325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
37da897
ea76f18
d9d24f0
0d3549e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@tanstack/start-server-core': patch | ||
| --- | ||
|
|
||
| fix(start-server-core): fall back HEAD requests to GET then ANY per RFC 9110 §9.3.2 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { createFileRoute } from '@tanstack/react-router' | ||
|
|
||
| export const Route = createFileRoute('/api/get-and-any')({ | ||
| server: { | ||
| handlers: { | ||
| GET: () => | ||
| new Response(null, { | ||
| headers: { 'x-handler': 'GET' }, | ||
| }), | ||
| ANY: ({ request }) => | ||
| new Response(null, { | ||
| headers: { 'x-handler': 'ANY', 'x-method': request.method }, | ||
| }), | ||
| }, | ||
| }, | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { createFileRoute } from '@tanstack/react-router' | ||
|
|
||
| // A server-only route with only a GET handler. | ||
| // Used to test that HEAD requests fall back to the GET handler per RFC 9110. | ||
| export const Route = createFileRoute('/api/head-fallback')({ | ||
| server: { | ||
| handlers: { | ||
| GET: () => | ||
| new Response('body', { | ||
| headers: { 'content-type': 'application/xml; charset=utf-8' }, | ||
| }), | ||
| }, | ||
| }, | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { createFileRoute, redirect } from '@tanstack/react-router' | ||
|
|
||
| export const Route = createFileRoute('/api/head-redirect-fallback')({ | ||
| server: { | ||
| handlers: { | ||
| GET: () => redirect({ to: '/api/head-fallback', statusCode: 307 }), | ||
| }, | ||
| }, | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -891,14 +891,22 @@ async function handleServerRoutes({ | |
|
|
||
| // Add handler middleware if exact match | ||
| const server = foundRoute?.options.server | ||
| let isHeadFallback = false | ||
| if (server?.handlers && isExactMatch) { | ||
| const handlers = | ||
| typeof server.handlers === 'function' | ||
| ? server.handlers({ createHandlers: (d: any) => d }) | ||
| : server.handlers | ||
|
|
||
| const requestMethod = request.method.toUpperCase() as RouteMethod | ||
| const handler = handlers[requestMethod] ?? handlers['ANY'] | ||
| // Per RFC 9110 §9.3.2, HEAD must return the same header fields as GET. | ||
| // Priority for HEAD: explicit HEAD handler → GET → ANY (last resort). | ||
| const handler = | ||
| requestMethod === 'HEAD' | ||
| ? (handlers['HEAD'] ?? handlers['GET'] ?? handlers['ANY']) | ||
| : (handlers[requestMethod] ?? handlers['ANY']) | ||
| isHeadFallback = | ||
| requestMethod === 'HEAD' && handler !== undefined && !handlers['HEAD'] | ||
|
|
||
| if (handler) { | ||
| const mayDefer = !!foundRoute.options.component | ||
|
|
@@ -931,5 +939,16 @@ async function handleServerRoutes({ | |
| pathname, | ||
| }) | ||
|
|
||
| // RFC 9110 §9.3.2: HEAD must carry the same header fields as GET but no body. | ||
| // Resolve any redirect before stripping so the Location header survives. | ||
| if (isHeadFallback) { | ||
| const resolved = await handleRedirectResponse( | ||
| ctx.response as Response, | ||
| request, | ||
| getRouter, | ||
| ) | ||
| return new Response(null, resolved) | ||
|
Comment on lines
+944
to
+950
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't synthesize a 200 OK when no response was produced. Line 946 forces Suggested fix if (isHeadFallback) {
+ if (!ctx.response) {
+ throwRouteHandlerError()
+ }
+
const resolved = await handleRedirectResponse(
- ctx.response as Response,
+ ctx.response,
request,
getRouter,
)
return new Response(null, resolved)
}As per coding guidelines, 🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this makes sense. please incorporate
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Why this matters: Failed to handle agent chat message. Please try again. |
||
| } | ||
|
|
||
| return ctx.response | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if both GET and ANY exist, HEAD uses ANY, not GET.
could be solved via:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add a separate E2E test route for this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Handler selection is now
handlers['HEAD'] ?? handlers['GET'] ?? handlers['ANY'], so GET always wins over ANY for HEAD requests.Added
/api/get-and-anywith separate GET and ANY handlers, and a test that sendsHEAD /api/get-and-anyand assertsx-handler: GET.