From 16d4a8a7df183f5cb486ac615effdf9f15e56b55 Mon Sep 17 00:00:00 2001 From: Hibbaan Date: Wed, 5 Nov 2025 18:08:46 +1100 Subject: [PATCH 1/3] fix(start-server-core): return 404 for API routes without GET handler --- .../src/createStartHandler.ts | 26 +++++- .../tests/createStartHandler.test.ts | 81 +++++++++++++++++++ .../tests/mocks/injected-head-scripts.ts | 1 + .../tests/mocks/router-entry.ts | 30 +++++++ .../tests/mocks/start-entry.ts | 7 ++ .../tests/mocks/start-manifest.ts | 10 +++ packages/start-server-core/vite.config.ts | 11 ++- 7 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 packages/start-server-core/tests/createStartHandler.test.ts create mode 100644 packages/start-server-core/tests/mocks/injected-head-scripts.ts create mode 100644 packages/start-server-core/tests/mocks/router-entry.ts create mode 100644 packages/start-server-core/tests/mocks/start-entry.ts create mode 100644 packages/start-server-core/tests/mocks/start-manifest.ts diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts index a304f33dfd3..48fc687e51c 100644 --- a/packages/start-server-core/src/createStartHandler.ts +++ b/packages/start-server-core/src/createStartHandler.ts @@ -380,12 +380,32 @@ async function handleServerRoutes({ createHandlers: (d: any) => d, }) : server.handlers + const normalizedHandlers: Record = {} + for (const [k, v] of Object.entries(handlers)) { + normalizedHandlers[k.toUpperCase()] = v + } - const requestMethod = request.method.toUpperCase() as RouteMethod - + let requestMethod = request.method.toUpperCase() as RouteMethod + if (requestMethod === 'HEAD' && normalizedHandlers["GET"]) { + requestMethod = 'GET' as RouteMethod + } + const hasAny = !!normalizedHandlers["ANY"] // Attempt to find the method in the handlers - const handler = handlers[requestMethod] ?? handlers['ANY'] + const handler = normalizedHandlers[requestMethod] ?? normalizedHandlers["ANY"] + if (!handler && !hasAny) { + if (request.method.toUpperCase() === 'HEAD') { + return new Response(null, { + status: 404, + headers: { 'Content-Type': 'application/json' }, + }) + } + + return new Response(JSON.stringify({ error: 'Not Found' }), { + status: 404, + headers: { 'Content-Type': 'application/json' }, + }) + } // If a method is found, execute the handler if (handler) { const mayDefer = !!foundRoute.options.component diff --git a/packages/start-server-core/tests/createStartHandler.test.ts b/packages/start-server-core/tests/createStartHandler.test.ts new file mode 100644 index 00000000000..75e49da104b --- /dev/null +++ b/packages/start-server-core/tests/createStartHandler.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { createStartHandler } from '../src' +import { currentHandlers } from './mocks/router-entry' + + +const spaFallback = async () => + new Response('
spa
', { + status: 200, + headers: { 'Content-Type': 'text/html' }, + }) + +function makeApp() { + return createStartHandler(async () => await spaFallback()) +} +beforeEach(() => { + Object.keys(currentHandlers).forEach(key => delete currentHandlers[key]) +}) + +describe('createStartHandler — server route HTTP method handling', function () { + it('should return 404 JSON for GET when only POST is defined (no SPA fallback)', async function () { + currentHandlers.POST = () => new Response('ok', { status: 200 }) + const app = makeApp() + + const res = await app( + new Request('http://localhost/api/test-no-get', { method: 'GET' }), + ) + + expect(res.status).toBe(404) + expect(res.headers.get('content-type')).toMatch(/application\/json/i) + const txt = await res.text() + expect(txt).toContain('Not Found') + expect(txt.toLowerCase().startsWith('')).toBe(false) + }) + + it('should return 200 for POST and execute the route handler', async function () { + currentHandlers.POST = () => new Response('ok', { status: 200 }) + const app = makeApp() + + const res = await app( + new Request('http://localhost/api/test-no-get', { method: 'POST' }), + ) + + expect(res.status).toBe(200) + expect(await res.text()).toBe('ok') + }) + + it('should return 404 for HEAD when GET is not defined', async function () { + currentHandlers.POST = () => new Response('ok', { status: 200 }) + const app = makeApp() + + const res = await app( + new Request('http://localhost/api/test-no-get', { method: 'HEAD' }), + ) + + expect(res.status).toBe(404) + }) + + it('should use GET handler when HEAD is requested and GET exists', async function () { + currentHandlers.GET = () => new Response('hello', { status: 200 }) + const app = makeApp() + + const res = await app( + new Request('http://localhost/api/has-get', { method: 'HEAD' }), + ) + + expect(res.status).toBe(200) + }) + + it('should execute ANY handler for unsupported methods (e.g., PUT)', async function () { + currentHandlers.ANY = () => new Response('ok-any', { status: 200 }) + const app = makeApp() + + const res = await app( + new Request('http://localhost/api/any', { method: 'PUT' }), + ) + + expect(res.status).toBe(200) + expect(await res.text()).toBe('ok-any') + }) +}) + diff --git a/packages/start-server-core/tests/mocks/injected-head-scripts.ts b/packages/start-server-core/tests/mocks/injected-head-scripts.ts new file mode 100644 index 00000000000..8af9e8ab1f1 --- /dev/null +++ b/packages/start-server-core/tests/mocks/injected-head-scripts.ts @@ -0,0 +1 @@ +export const injectedHeadScripts = '' \ No newline at end of file diff --git a/packages/start-server-core/tests/mocks/router-entry.ts b/packages/start-server-core/tests/mocks/router-entry.ts new file mode 100644 index 00000000000..29f4c9bce10 --- /dev/null +++ b/packages/start-server-core/tests/mocks/router-entry.ts @@ -0,0 +1,30 @@ +import { AnyRouter } from '@tanstack/router-core' + +export let currentHandlers: Record = {} + +function makeFakeRouter(): AnyRouter { + return { + rewrite: undefined as any, + getMatchedRoutes: (_pathname: string) => ({ + matchedRoutes: [{ options: { server: { middleware: [] } } }], + foundRoute: { + options: { + server: { handlers: currentHandlers }, + component: undefined, + }, + }, + routeParams: {}, + }), + + update: () => {}, + load: async () => {}, + state: { redirect: null } as any, + serverSsr: { dehydrate: async () => {} } as any, + options: {} as any, + resolveRedirect: (r: any) => r, + } as unknown as AnyRouter +} + +export async function getRouter() { + return makeFakeRouter() +} \ No newline at end of file diff --git a/packages/start-server-core/tests/mocks/start-entry.ts b/packages/start-server-core/tests/mocks/start-entry.ts new file mode 100644 index 00000000000..108522bf48b --- /dev/null +++ b/packages/start-server-core/tests/mocks/start-entry.ts @@ -0,0 +1,7 @@ +export const startInstance = { + getOptions: async () => ({ + requestMiddleware: undefined, + defaultSsr: undefined, + serializationAdapters: [], + }), +} \ No newline at end of file diff --git a/packages/start-server-core/tests/mocks/start-manifest.ts b/packages/start-server-core/tests/mocks/start-manifest.ts new file mode 100644 index 00000000000..e8f8a9b3fb9 --- /dev/null +++ b/packages/start-server-core/tests/mocks/start-manifest.ts @@ -0,0 +1,10 @@ +export const tsrStartManifest = () => ({ + routes: { + __root__: { + id: '__root__', + }, + }, + routeTree: { + id: '__root__', + }, +}) \ No newline at end of file diff --git a/packages/start-server-core/vite.config.ts b/packages/start-server-core/vite.config.ts index f0ca2cc699f..31de106afaf 100644 --- a/packages/start-server-core/vite.config.ts +++ b/packages/start-server-core/vite.config.ts @@ -4,7 +4,7 @@ import packageJson from './package.json' // this needs to be imported from the actual file instead of from 'index.tsx' // so we don't trigger the import of a `?script-string` import before the minifyScriptPlugin is setup import { VIRTUAL_MODULES } from './src/virtual-modules' - +import path from 'path' const config = defineConfig({ test: { include: ['**/*.{test-d,test,spec}.?(c|m)[jt]s?(x)'], @@ -12,6 +12,15 @@ const config = defineConfig({ watch: false, environment: 'jsdom', }, + resolve: { + alias: { + '#tanstack-router-entry': path.resolve(__dirname, './tests/mocks/router-entry.ts'), + '#tanstack-start-entry': path.resolve(__dirname, './tests/mocks/start-entry.ts'), + 'tanstack-start-manifest:v': path.resolve(__dirname, './tests/mocks/start-manifest.ts'), + 'tanstack-start-injected-head-scripts:v': path.resolve(__dirname, './tests/mocks/injected-head-scripts.ts'), + + } + } }) export default mergeConfig( From a5d9c70d81eb45b6016bf6f8d954a60318ec22c7 Mon Sep 17 00:00:00 2001 From: Hibbaan Date: Thu, 6 Nov 2025 12:36:18 +1100 Subject: [PATCH 2/3] lint changes: apply review suggestions (import ordering, type-only import, const) --- packages/start-server-core/tests/createStartHandler.test.ts | 2 +- packages/start-server-core/tests/mocks/router-entry.ts | 4 ++-- packages/start-server-core/vite.config.ts | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/start-server-core/tests/createStartHandler.test.ts b/packages/start-server-core/tests/createStartHandler.test.ts index 75e49da104b..9ea0d6e4f11 100644 --- a/packages/start-server-core/tests/createStartHandler.test.ts +++ b/packages/start-server-core/tests/createStartHandler.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from 'vitest' +import { beforeEach, describe, expect, it } from 'vitest' import { createStartHandler } from '../src' import { currentHandlers } from './mocks/router-entry' diff --git a/packages/start-server-core/tests/mocks/router-entry.ts b/packages/start-server-core/tests/mocks/router-entry.ts index 29f4c9bce10..48a829a5e06 100644 --- a/packages/start-server-core/tests/mocks/router-entry.ts +++ b/packages/start-server-core/tests/mocks/router-entry.ts @@ -1,6 +1,6 @@ -import { AnyRouter } from '@tanstack/router-core' +import type { AnyRouter } from '@tanstack/router-core' -export let currentHandlers: Record = {} +export const currentHandlers: Record = {} function makeFakeRouter(): AnyRouter { return { diff --git a/packages/start-server-core/vite.config.ts b/packages/start-server-core/vite.config.ts index 31de106afaf..bab55ea9826 100644 --- a/packages/start-server-core/vite.config.ts +++ b/packages/start-server-core/vite.config.ts @@ -1,10 +1,11 @@ +import path from 'node:path' import { defineConfig, mergeConfig } from 'vitest/config' import { tanstackViteConfig } from '@tanstack/config/vite' import packageJson from './package.json' // this needs to be imported from the actual file instead of from 'index.tsx' // so we don't trigger the import of a `?script-string` import before the minifyScriptPlugin is setup import { VIRTUAL_MODULES } from './src/virtual-modules' -import path from 'path' + const config = defineConfig({ test: { include: ['**/*.{test-d,test,spec}.?(c|m)[jt]s?(x)'], From e3ccdddf24fdff44c2d764fe68953490b282a664 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 20:48:55 +0000 Subject: [PATCH 3/3] ci: apply automated fixes --- .../src/createStartHandler.ts | 10 ++++---- .../tests/createStartHandler.test.ts | 4 +-- .../tests/mocks/injected-head-scripts.ts | 2 +- .../tests/mocks/router-entry.ts | 6 ++--- .../tests/mocks/start-entry.ts | 2 +- .../tests/mocks/start-manifest.ts | 2 +- packages/start-server-core/vite.config.ts | 25 +++++++++++++------ 7 files changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts index 48fc687e51c..b53351a5249 100644 --- a/packages/start-server-core/src/createStartHandler.ts +++ b/packages/start-server-core/src/createStartHandler.ts @@ -386,18 +386,18 @@ async function handleServerRoutes({ } let requestMethod = request.method.toUpperCase() as RouteMethod - if (requestMethod === 'HEAD' && normalizedHandlers["GET"]) { + if (requestMethod === 'HEAD' && normalizedHandlers['GET']) { requestMethod = 'GET' as RouteMethod } - const hasAny = !!normalizedHandlers["ANY"] + const hasAny = !!normalizedHandlers['ANY'] // Attempt to find the method in the handlers - const handler = normalizedHandlers[requestMethod] ?? normalizedHandlers["ANY"] + const handler = + normalizedHandlers[requestMethod] ?? normalizedHandlers['ANY'] if (!handler && !hasAny) { - if (request.method.toUpperCase() === 'HEAD') { return new Response(null, { status: 404, - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' }, }) } diff --git a/packages/start-server-core/tests/createStartHandler.test.ts b/packages/start-server-core/tests/createStartHandler.test.ts index 9ea0d6e4f11..adb0ec9484c 100644 --- a/packages/start-server-core/tests/createStartHandler.test.ts +++ b/packages/start-server-core/tests/createStartHandler.test.ts @@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it } from 'vitest' import { createStartHandler } from '../src' import { currentHandlers } from './mocks/router-entry' - const spaFallback = async () => new Response('
spa
', { status: 200, @@ -13,7 +12,7 @@ function makeApp() { return createStartHandler(async () => await spaFallback()) } beforeEach(() => { - Object.keys(currentHandlers).forEach(key => delete currentHandlers[key]) + Object.keys(currentHandlers).forEach((key) => delete currentHandlers[key]) }) describe('createStartHandler — server route HTTP method handling', function () { @@ -78,4 +77,3 @@ describe('createStartHandler — server route HTTP method handling', function () expect(await res.text()).toBe('ok-any') }) }) - diff --git a/packages/start-server-core/tests/mocks/injected-head-scripts.ts b/packages/start-server-core/tests/mocks/injected-head-scripts.ts index 8af9e8ab1f1..9e98c1c1672 100644 --- a/packages/start-server-core/tests/mocks/injected-head-scripts.ts +++ b/packages/start-server-core/tests/mocks/injected-head-scripts.ts @@ -1 +1 @@ -export const injectedHeadScripts = '' \ No newline at end of file +export const injectedHeadScripts = '' diff --git a/packages/start-server-core/tests/mocks/router-entry.ts b/packages/start-server-core/tests/mocks/router-entry.ts index 48a829a5e06..98a7a99254e 100644 --- a/packages/start-server-core/tests/mocks/router-entry.ts +++ b/packages/start-server-core/tests/mocks/router-entry.ts @@ -10,12 +10,12 @@ function makeFakeRouter(): AnyRouter { foundRoute: { options: { server: { handlers: currentHandlers }, - component: undefined, + component: undefined, }, }, routeParams: {}, }), - + update: () => {}, load: async () => {}, state: { redirect: null } as any, @@ -27,4 +27,4 @@ function makeFakeRouter(): AnyRouter { export async function getRouter() { return makeFakeRouter() -} \ No newline at end of file +} diff --git a/packages/start-server-core/tests/mocks/start-entry.ts b/packages/start-server-core/tests/mocks/start-entry.ts index 108522bf48b..7d67fb23e41 100644 --- a/packages/start-server-core/tests/mocks/start-entry.ts +++ b/packages/start-server-core/tests/mocks/start-entry.ts @@ -4,4 +4,4 @@ export const startInstance = { defaultSsr: undefined, serializationAdapters: [], }), -} \ No newline at end of file +} diff --git a/packages/start-server-core/tests/mocks/start-manifest.ts b/packages/start-server-core/tests/mocks/start-manifest.ts index e8f8a9b3fb9..a2cc4ab9ec1 100644 --- a/packages/start-server-core/tests/mocks/start-manifest.ts +++ b/packages/start-server-core/tests/mocks/start-manifest.ts @@ -7,4 +7,4 @@ export const tsrStartManifest = () => ({ routeTree: { id: '__root__', }, -}) \ No newline at end of file +}) diff --git a/packages/start-server-core/vite.config.ts b/packages/start-server-core/vite.config.ts index bab55ea9826..d40904d740d 100644 --- a/packages/start-server-core/vite.config.ts +++ b/packages/start-server-core/vite.config.ts @@ -15,13 +15,24 @@ const config = defineConfig({ }, resolve: { alias: { - '#tanstack-router-entry': path.resolve(__dirname, './tests/mocks/router-entry.ts'), - '#tanstack-start-entry': path.resolve(__dirname, './tests/mocks/start-entry.ts'), - 'tanstack-start-manifest:v': path.resolve(__dirname, './tests/mocks/start-manifest.ts'), - 'tanstack-start-injected-head-scripts:v': path.resolve(__dirname, './tests/mocks/injected-head-scripts.ts'), - - } - } + '#tanstack-router-entry': path.resolve( + __dirname, + './tests/mocks/router-entry.ts', + ), + '#tanstack-start-entry': path.resolve( + __dirname, + './tests/mocks/start-entry.ts', + ), + 'tanstack-start-manifest:v': path.resolve( + __dirname, + './tests/mocks/start-manifest.ts', + ), + 'tanstack-start-injected-head-scripts:v': path.resolve( + __dirname, + './tests/mocks/injected-head-scripts.ts', + ), + }, + }, }) export default mergeConfig(