From 37da8975ae5cc206be12ecf62f830b6132937be4 Mon Sep 17 00:00:00 2001 From: Zelys Date: Sat, 2 May 2026 19:12:57 -0500 Subject: [PATCH 1/6] fix(start-server-core): fall back HEAD to GET then ANY per RFC 9110 - HEAD priority is now HEAD -> GET -> ANY (was HEAD -> ANY -> GET) - body stripping covers ANY fallback, not just GET fallback - resolve redirects before stripping so Location header is preserved - copy statusText to the stripped response - add E2E tests for all three HEAD fallback scenarios --- .../fix-head-request-server-route-fallback.md | 5 ++ .../server-routes/src/routeTree.gen.ts | 42 +++++++++++++++ .../src/routes/api/get-and-any.ts | 16 ++++++ .../src/routes/api/head-fallback.ts | 14 +++++ .../server-routes/tests/server-routes.spec.ts | 53 +++++++++++++++++++ .../src/createStartHandler.ts | 21 +++++++- 6 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-head-request-server-route-fallback.md create mode 100644 e2e/react-start/server-routes/src/routes/api/get-and-any.ts create mode 100644 e2e/react-start/server-routes/src/routes/api/head-fallback.ts diff --git a/.changeset/fix-head-request-server-route-fallback.md b/.changeset/fix-head-request-server-route-fallback.md new file mode 100644 index 00000000000..3b041bea997 --- /dev/null +++ b/.changeset/fix-head-request-server-route-fallback.md @@ -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 diff --git a/e2e/react-start/server-routes/src/routeTree.gen.ts b/e2e/react-start/server-routes/src/routeTree.gen.ts index 8caf1ab9179..674114a2818 100644 --- a/e2e/react-start/server-routes/src/routeTree.gen.ts +++ b/e2e/react-start/server-routes/src/routeTree.gen.ts @@ -16,6 +16,8 @@ import { Route as MethodsIndexRouteImport } from './routes/methods/index' import { Route as MethodsOnlyAnyRouteImport } from './routes/methods/only-any' import { Route as ApiOnlyAnyRouteImport } from './routes/api/only-any' import { Route as ApiMiddlewareContextRouteImport } from './routes/api/middleware-context' +import { Route as ApiHeadFallbackRouteImport } from './routes/api/head-fallback' +import { Route as ApiGetAndAnyRouteImport } from './routes/api/get-and-any' import { Route as ApiParamsFooRouteRouteImport } from './routes/api/params/$foo/route' import { Route as ApiParamsFooBarRouteImport } from './routes/api/params/$foo/$bar' @@ -49,6 +51,16 @@ const ApiOnlyAnyRoute = ApiOnlyAnyRouteImport.update({ path: '/api/only-any', getParentRoute: () => rootRouteImport, } as any) +const ApiHeadFallbackRoute = ApiHeadFallbackRouteImport.update({ + id: '/api/head-fallback', + path: '/api/head-fallback', + getParentRoute: () => rootRouteImport, +} as any) +const ApiGetAndAnyRoute = ApiGetAndAnyRouteImport.update({ + id: '/api/get-and-any', + path: '/api/get-and-any', + getParentRoute: () => rootRouteImport, +} as any) const ApiMiddlewareContextRoute = ApiMiddlewareContextRouteImport.update({ id: '/api/middleware-context', path: '/api/middleware-context', @@ -71,6 +83,8 @@ export interface FileRoutesByFullPath { '/merge-middleware-context': typeof MergeMiddlewareContextRoute '/api/middleware-context': typeof ApiMiddlewareContextRoute '/api/only-any': typeof ApiOnlyAnyRoute + '/api/head-fallback': typeof ApiHeadFallbackRoute + '/api/get-and-any': typeof ApiGetAndAnyRoute '/methods/only-any': typeof MethodsOnlyAnyRoute '/methods/': typeof MethodsIndexRoute '/api/params/$foo': typeof ApiParamsFooRouteRouteWithChildren @@ -81,6 +95,8 @@ export interface FileRoutesByTo { '/merge-middleware-context': typeof MergeMiddlewareContextRoute '/api/middleware-context': typeof ApiMiddlewareContextRoute '/api/only-any': typeof ApiOnlyAnyRoute + '/api/head-fallback': typeof ApiHeadFallbackRoute + '/api/get-and-any': typeof ApiGetAndAnyRoute '/methods/only-any': typeof MethodsOnlyAnyRoute '/methods': typeof MethodsIndexRoute '/api/params/$foo': typeof ApiParamsFooRouteRouteWithChildren @@ -93,6 +109,8 @@ export interface FileRoutesById { '/merge-middleware-context': typeof MergeMiddlewareContextRoute '/api/middleware-context': typeof ApiMiddlewareContextRoute '/api/only-any': typeof ApiOnlyAnyRoute + '/api/head-fallback': typeof ApiHeadFallbackRoute + '/api/get-and-any': typeof ApiGetAndAnyRoute '/methods/only-any': typeof MethodsOnlyAnyRoute '/methods/': typeof MethodsIndexRoute '/api/params/$foo': typeof ApiParamsFooRouteRouteWithChildren @@ -106,6 +124,8 @@ export interface FileRouteTypes { | '/merge-middleware-context' | '/api/middleware-context' | '/api/only-any' + | '/api/head-fallback' + | '/api/get-and-any' | '/methods/only-any' | '/methods/' | '/api/params/$foo' @@ -116,6 +136,8 @@ export interface FileRouteTypes { | '/merge-middleware-context' | '/api/middleware-context' | '/api/only-any' + | '/api/head-fallback' + | '/api/get-and-any' | '/methods/only-any' | '/methods' | '/api/params/$foo' @@ -127,6 +149,8 @@ export interface FileRouteTypes { | '/merge-middleware-context' | '/api/middleware-context' | '/api/only-any' + | '/api/head-fallback' + | '/api/get-and-any' | '/methods/only-any' | '/methods/' | '/api/params/$foo' @@ -139,6 +163,8 @@ export interface RootRouteChildren { MergeMiddlewareContextRoute: typeof MergeMiddlewareContextRoute ApiMiddlewareContextRoute: typeof ApiMiddlewareContextRoute ApiOnlyAnyRoute: typeof ApiOnlyAnyRoute + ApiHeadFallbackRoute: typeof ApiHeadFallbackRoute + ApiGetAndAnyRoute: typeof ApiGetAndAnyRoute ApiParamsFooRouteRoute: typeof ApiParamsFooRouteRouteWithChildren } @@ -186,6 +212,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiOnlyAnyRouteImport parentRoute: typeof rootRouteImport } + '/api/head-fallback': { + id: '/api/head-fallback' + path: '/api/head-fallback' + fullPath: '/api/head-fallback' + preLoaderRoute: typeof ApiHeadFallbackRouteImport + parentRoute: typeof rootRouteImport + } + '/api/get-and-any': { + id: '/api/get-and-any' + path: '/api/get-and-any' + fullPath: '/api/get-and-any' + preLoaderRoute: typeof ApiGetAndAnyRouteImport + parentRoute: typeof rootRouteImport + } '/api/middleware-context': { id: '/api/middleware-context' path: '/api/middleware-context' @@ -241,6 +281,8 @@ const rootRouteChildren: RootRouteChildren = { MergeMiddlewareContextRoute: MergeMiddlewareContextRoute, ApiMiddlewareContextRoute: ApiMiddlewareContextRoute, ApiOnlyAnyRoute: ApiOnlyAnyRoute, + ApiHeadFallbackRoute: ApiHeadFallbackRoute, + ApiGetAndAnyRoute: ApiGetAndAnyRoute, ApiParamsFooRouteRoute: ApiParamsFooRouteRouteWithChildren, } export const routeTree = rootRouteImport diff --git a/e2e/react-start/server-routes/src/routes/api/get-and-any.ts b/e2e/react-start/server-routes/src/routes/api/get-and-any.ts new file mode 100644 index 00000000000..4e3ed08fbbe --- /dev/null +++ b/e2e/react-start/server-routes/src/routes/api/get-and-any.ts @@ -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 }, + }), + }, + }, +}) diff --git a/e2e/react-start/server-routes/src/routes/api/head-fallback.ts b/e2e/react-start/server-routes/src/routes/api/head-fallback.ts new file mode 100644 index 00000000000..302ba81efde --- /dev/null +++ b/e2e/react-start/server-routes/src/routes/api/head-fallback.ts @@ -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' }, + }), + }, + }, +}) diff --git a/e2e/react-start/server-routes/tests/server-routes.spec.ts b/e2e/react-start/server-routes/tests/server-routes.spec.ts index f3a80a5a469..ce2e5f60227 100644 --- a/e2e/react-start/server-routes/tests/server-routes.spec.ts +++ b/e2e/react-start/server-routes/tests/server-routes.spec.ts @@ -17,6 +17,59 @@ test('merge-middleware-context', async ({ page }) => { expect(contextResult).toContain('test') }) +test.describe('HEAD fallback', () => { + test('strips body and preserves headers when falling back to GET', async ({ + page, + }) => { + await page.goto('/') + const result = await page.evaluate(async () => { + const res = await fetch('/api/head-fallback', { method: 'HEAD' }) + return { + status: res.status, + contentType: res.headers.get('content-type'), + body: await res.text(), + } + }) + expect(result.status).toBe(200) + expect(result.contentType).toBe('application/xml; charset=utf-8') + expect(result.body).toBe('') + }) + + test('strips body and preserves headers when falling back to ANY', async ({ + page, + }) => { + await page.goto('/') + const result = await page.evaluate(async () => { + const res = await fetch('/api/only-any', { method: 'HEAD' }) + return { + status: res.status, + xHandler: res.headers.get('x-handler'), + xMethod: res.headers.get('x-method'), + body: await res.text(), + } + }) + expect(result.status).toBe(200) + expect(result.xHandler).toBe('ANY') + expect(result.xMethod).toBe('HEAD') + expect(result.body).toBe('') + }) + + test('prefers GET over ANY for HEAD requests', async ({ page }) => { + await page.goto('/') + const result = await page.evaluate(async () => { + const res = await fetch('/api/get-and-any', { method: 'HEAD' }) + return { + status: res.status, + xHandler: res.headers.get('x-handler'), + body: await res.text(), + } + }) + expect(result.status).toBe(200) + expect(result.xHandler).toBe('GET') + expect(result.body).toBe('') + }) +}) + test.describe('methods', () => { test('only ANY', async ({ page }) => { await page.goto('/methods/only-any') diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts index a666ed87c11..ba2e7c4f471 100644 --- a/packages/start-server-core/src/createStartHandler.ts +++ b/packages/start-server-core/src/createStartHandler.ts @@ -891,6 +891,7 @@ 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' @@ -898,7 +899,14 @@ async function handleServerRoutes({ : 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 && ctx.response instanceof Response) { + const resolved = await handleRedirectResponse(ctx.response, request, getRouter) + return new Response(null, { + status: resolved.status, + statusText: resolved.statusText, + headers: resolved.headers, + }) + } + return ctx.response } From ea76f18880bc3b1d9551f1c26e93ba6af3867819 Mon Sep 17 00:00:00 2001 From: Zelys Date: Sat, 2 May 2026 19:48:05 -0500 Subject: [PATCH 2/6] refactor(start-server-core): drop redundant instanceof guard, add redirect+HEAD E2E test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `instanceof Response` check before the HEAD body-stripping block was redundant — `redirect()` returns `new Response(...)` and IS instanceof Response, and `ctx.response` is always a Response at that point in the middleware pipeline. Replacing it with an `as Response` cast eliminates dead branch analysis noise. Also adds a fourth E2E test: HEAD on a route whose GET handler returns a `redirect()` must preserve the Location header so the browser can follow it. The test verifies this by asserting the final status is 200 (meaning the 307 redirect was followed). If the Location header were stripped before the redirect resolved, the browser would stall on the 307 and the assertion would fail. --- .../server-routes/src/routeTree.gen.ts | 21 +++++++++++++++++++ .../src/routes/api/head-redirect-fallback.ts | 9 ++++++++ .../server-routes/tests/server-routes.spec.ts | 20 ++++++++++++++++++ .../src/createStartHandler.ts | 8 +++++-- 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 e2e/react-start/server-routes/src/routes/api/head-redirect-fallback.ts diff --git a/e2e/react-start/server-routes/src/routeTree.gen.ts b/e2e/react-start/server-routes/src/routeTree.gen.ts index 674114a2818..1c32040cef7 100644 --- a/e2e/react-start/server-routes/src/routeTree.gen.ts +++ b/e2e/react-start/server-routes/src/routeTree.gen.ts @@ -18,6 +18,7 @@ import { Route as ApiOnlyAnyRouteImport } from './routes/api/only-any' import { Route as ApiMiddlewareContextRouteImport } from './routes/api/middleware-context' import { Route as ApiHeadFallbackRouteImport } from './routes/api/head-fallback' import { Route as ApiGetAndAnyRouteImport } from './routes/api/get-and-any' +import { Route as ApiHeadRedirectFallbackRouteImport } from './routes/api/head-redirect-fallback' import { Route as ApiParamsFooRouteRouteImport } from './routes/api/params/$foo/route' import { Route as ApiParamsFooBarRouteImport } from './routes/api/params/$foo/$bar' @@ -61,6 +62,11 @@ const ApiGetAndAnyRoute = ApiGetAndAnyRouteImport.update({ path: '/api/get-and-any', getParentRoute: () => rootRouteImport, } as any) +const ApiHeadRedirectFallbackRoute = ApiHeadRedirectFallbackRouteImport.update({ + id: '/api/head-redirect-fallback', + path: '/api/head-redirect-fallback', + getParentRoute: () => rootRouteImport, +} as any) const ApiMiddlewareContextRoute = ApiMiddlewareContextRouteImport.update({ id: '/api/middleware-context', path: '/api/middleware-context', @@ -85,6 +91,7 @@ export interface FileRoutesByFullPath { '/api/only-any': typeof ApiOnlyAnyRoute '/api/head-fallback': typeof ApiHeadFallbackRoute '/api/get-and-any': typeof ApiGetAndAnyRoute + '/api/head-redirect-fallback': typeof ApiHeadRedirectFallbackRoute '/methods/only-any': typeof MethodsOnlyAnyRoute '/methods/': typeof MethodsIndexRoute '/api/params/$foo': typeof ApiParamsFooRouteRouteWithChildren @@ -97,6 +104,7 @@ export interface FileRoutesByTo { '/api/only-any': typeof ApiOnlyAnyRoute '/api/head-fallback': typeof ApiHeadFallbackRoute '/api/get-and-any': typeof ApiGetAndAnyRoute + '/api/head-redirect-fallback': typeof ApiHeadRedirectFallbackRoute '/methods/only-any': typeof MethodsOnlyAnyRoute '/methods': typeof MethodsIndexRoute '/api/params/$foo': typeof ApiParamsFooRouteRouteWithChildren @@ -111,6 +119,7 @@ export interface FileRoutesById { '/api/only-any': typeof ApiOnlyAnyRoute '/api/head-fallback': typeof ApiHeadFallbackRoute '/api/get-and-any': typeof ApiGetAndAnyRoute + '/api/head-redirect-fallback': typeof ApiHeadRedirectFallbackRoute '/methods/only-any': typeof MethodsOnlyAnyRoute '/methods/': typeof MethodsIndexRoute '/api/params/$foo': typeof ApiParamsFooRouteRouteWithChildren @@ -126,6 +135,7 @@ export interface FileRouteTypes { | '/api/only-any' | '/api/head-fallback' | '/api/get-and-any' + | '/api/head-redirect-fallback' | '/methods/only-any' | '/methods/' | '/api/params/$foo' @@ -138,6 +148,7 @@ export interface FileRouteTypes { | '/api/only-any' | '/api/head-fallback' | '/api/get-and-any' + | '/api/head-redirect-fallback' | '/methods/only-any' | '/methods' | '/api/params/$foo' @@ -151,6 +162,7 @@ export interface FileRouteTypes { | '/api/only-any' | '/api/head-fallback' | '/api/get-and-any' + | '/api/head-redirect-fallback' | '/methods/only-any' | '/methods/' | '/api/params/$foo' @@ -165,6 +177,7 @@ export interface RootRouteChildren { ApiOnlyAnyRoute: typeof ApiOnlyAnyRoute ApiHeadFallbackRoute: typeof ApiHeadFallbackRoute ApiGetAndAnyRoute: typeof ApiGetAndAnyRoute + ApiHeadRedirectFallbackRoute: typeof ApiHeadRedirectFallbackRoute ApiParamsFooRouteRoute: typeof ApiParamsFooRouteRouteWithChildren } @@ -226,6 +239,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiGetAndAnyRouteImport parentRoute: typeof rootRouteImport } + '/api/head-redirect-fallback': { + id: '/api/head-redirect-fallback' + path: '/api/head-redirect-fallback' + fullPath: '/api/head-redirect-fallback' + preLoaderRoute: typeof ApiHeadRedirectFallbackRouteImport + parentRoute: typeof rootRouteImport + } '/api/middleware-context': { id: '/api/middleware-context' path: '/api/middleware-context' @@ -283,6 +303,7 @@ const rootRouteChildren: RootRouteChildren = { ApiOnlyAnyRoute: ApiOnlyAnyRoute, ApiHeadFallbackRoute: ApiHeadFallbackRoute, ApiGetAndAnyRoute: ApiGetAndAnyRoute, + ApiHeadRedirectFallbackRoute: ApiHeadRedirectFallbackRoute, ApiParamsFooRouteRoute: ApiParamsFooRouteRouteWithChildren, } export const routeTree = rootRouteImport diff --git a/e2e/react-start/server-routes/src/routes/api/head-redirect-fallback.ts b/e2e/react-start/server-routes/src/routes/api/head-redirect-fallback.ts new file mode 100644 index 00000000000..62e3998c6d7 --- /dev/null +++ b/e2e/react-start/server-routes/src/routes/api/head-redirect-fallback.ts @@ -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 }), + }, + }, +}) diff --git a/e2e/react-start/server-routes/tests/server-routes.spec.ts b/e2e/react-start/server-routes/tests/server-routes.spec.ts index ce2e5f60227..97249fd733e 100644 --- a/e2e/react-start/server-routes/tests/server-routes.spec.ts +++ b/e2e/react-start/server-routes/tests/server-routes.spec.ts @@ -68,6 +68,26 @@ test.describe('HEAD fallback', () => { expect(result.xHandler).toBe('GET') expect(result.body).toBe('') }) + + test('preserves Location header when GET handler returns a redirect', async ({ + page, + }) => { + await page.goto('/') + // HEAD /api/head-redirect-fallback → server returns 307 Location:/api/head-fallback + // Browser follows the redirect: HEAD /api/head-fallback → 200 with correct headers + // If the Location header were lost, the browser could not follow and would see a 307. + const result = await page.evaluate(async () => { + const res = await fetch('/api/head-redirect-fallback', { method: 'HEAD' }) + return { + status: res.status, + contentType: res.headers.get('content-type'), + body: await res.text(), + } + }) + expect(result.status).toBe(200) + expect(result.contentType).toBe('application/xml; charset=utf-8') + expect(result.body).toBe('') + }) }) test.describe('methods', () => { diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts index ba2e7c4f471..f3f37efebbc 100644 --- a/packages/start-server-core/src/createStartHandler.ts +++ b/packages/start-server-core/src/createStartHandler.ts @@ -941,8 +941,12 @@ async function handleServerRoutes({ // 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 && ctx.response instanceof Response) { - const resolved = await handleRedirectResponse(ctx.response, request, getRouter) + if (isHeadFallback) { + const resolved = await handleRedirectResponse( + ctx.response as Response, + request, + getRouter, + ) return new Response(null, { status: resolved.status, statusText: resolved.statusText, From d9d24f02c640b70a80fab9bf1ff9d9bd156de265 Mon Sep 17 00:00:00 2001 From: Zelys Date: Sat, 2 May 2026 20:11:29 -0500 Subject: [PATCH 3/6] refactor(start-server-core): use new Response(null, resolved) per reviewer suggestion Adopts the tighter form suggested by @schiller-manuel: passing the resolved Response directly as the ResponseInit arg copies status, statusText, and headers in one shot instead of spreading them manually. --- packages/start-server-core/src/createStartHandler.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts index f3f37efebbc..d4af46491b4 100644 --- a/packages/start-server-core/src/createStartHandler.ts +++ b/packages/start-server-core/src/createStartHandler.ts @@ -947,11 +947,7 @@ async function handleServerRoutes({ request, getRouter, ) - return new Response(null, { - status: resolved.status, - statusText: resolved.statusText, - headers: resolved.headers, - }) + return new Response(null, resolved) } return ctx.response From 0d3549e06c25d20b74153ba225c74989b63c0a9d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 08:18:07 +0000 Subject: [PATCH 4/6] ci: apply automated fixes --- .changeset/fix-head-request-server-route-fallback.md | 2 +- packages/start-server-core/src/createStartHandler.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/fix-head-request-server-route-fallback.md b/.changeset/fix-head-request-server-route-fallback.md index 3b041bea997..21572fc00be 100644 --- a/.changeset/fix-head-request-server-route-fallback.md +++ b/.changeset/fix-head-request-server-route-fallback.md @@ -1,5 +1,5 @@ --- -"@tanstack/start-server-core": patch +'@tanstack/start-server-core': patch --- fix(start-server-core): fall back HEAD requests to GET then ANY per RFC 9110 §9.3.2 diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts index d4af46491b4..b831a452ead 100644 --- a/packages/start-server-core/src/createStartHandler.ts +++ b/packages/start-server-core/src/createStartHandler.ts @@ -903,8 +903,8 @@ async function handleServerRoutes({ // Priority for HEAD: explicit HEAD handler → GET → ANY (last resort). const handler = requestMethod === 'HEAD' - ? handlers['HEAD'] ?? handlers['GET'] ?? handlers['ANY'] - : handlers[requestMethod] ?? handlers['ANY'] + ? (handlers['HEAD'] ?? handlers['GET'] ?? handlers['ANY']) + : (handlers[requestMethod] ?? handlers['ANY']) isHeadFallback = requestMethod === 'HEAD' && handler !== undefined && !handlers['HEAD'] From 97f09f4d542c800f5492853d89bd9f65c46dc4c7 Mon Sep 17 00:00:00 2001 From: Zelys Date: Sun, 3 May 2026 11:28:40 -0500 Subject: [PATCH 5/6] fix(start-server-core): guard against undefined ctx.response in HEAD fallback If a fallback GET/ANY handler exits without returning a Response, ctx.response is undefined. Without the guard, handleRedirectResponse passes undefined through and new Response(null, undefined) silently synthesizes a 200 OK, masking the handler bug. Calling throwRouteHandlerError() surfaces the mistake with the same error used everywhere else in the file. Also removes the unsafe cast -- TypeScript narrows the type correctly after the never-returning guard. --- packages/start-server-core/src/createStartHandler.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts index b831a452ead..1b59d942aa9 100644 --- a/packages/start-server-core/src/createStartHandler.ts +++ b/packages/start-server-core/src/createStartHandler.ts @@ -903,8 +903,8 @@ async function handleServerRoutes({ // Priority for HEAD: explicit HEAD handler → GET → ANY (last resort). const handler = requestMethod === 'HEAD' - ? (handlers['HEAD'] ?? handlers['GET'] ?? handlers['ANY']) - : (handlers[requestMethod] ?? handlers['ANY']) + ? handlers['HEAD'] ?? handlers['GET'] ?? handlers['ANY'] + : handlers[requestMethod] ?? handlers['ANY'] isHeadFallback = requestMethod === 'HEAD' && handler !== undefined && !handlers['HEAD'] @@ -942,8 +942,12 @@ async function handleServerRoutes({ // 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) { + if (!ctx.response) { + throwRouteHandlerError() + } + const resolved = await handleRedirectResponse( - ctx.response as Response, + ctx.response, request, getRouter, ) From 5e6b55982f2ef8fb2e9c2ba65dd8b5842b621305 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 16:33:30 +0000 Subject: [PATCH 6/6] ci: apply automated fixes --- packages/start-server-core/src/createStartHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts index 1b59d942aa9..eb444f2d583 100644 --- a/packages/start-server-core/src/createStartHandler.ts +++ b/packages/start-server-core/src/createStartHandler.ts @@ -903,8 +903,8 @@ async function handleServerRoutes({ // Priority for HEAD: explicit HEAD handler → GET → ANY (last resort). const handler = requestMethod === 'HEAD' - ? handlers['HEAD'] ?? handlers['GET'] ?? handlers['ANY'] - : handlers[requestMethod] ?? handlers['ANY'] + ? (handlers['HEAD'] ?? handlers['GET'] ?? handlers['ANY']) + : (handlers[requestMethod] ?? handlers['ANY']) isHeadFallback = requestMethod === 'HEAD' && handler !== undefined && !handlers['HEAD']