From 8c0a3a1a340b10409c84f752e45dad739608d0b6 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 23 Sep 2026 09:10:30 +0200 Subject: [PATCH] fix(tanstackstart-react): Reject non-POST requests to the managed tunnel route The managed tunnel route only defined a POST handler, so other methods fell through to SSR-rendering the app, running root route loaders. Add an ANY handler that returns 405. Fixes #24605 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../tanstackstart-react/tests/tunnel.test.ts | 9 +++++++++ packages/tanstackstart-react/src/vite/tunnelRoute.ts | 4 ++++ .../tanstackstart-react/test/vite/tunnelRoute.test.ts | 9 +++++++++ 3 files changed, 22 insertions(+) diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts index e6b9dfab05e0..9857d951b2d8 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts @@ -61,6 +61,15 @@ test('Sends client-side errors through the configured tunnel route', async ({ pa expect(errorEvent.transaction).toBe('/'); }); +test('Rejects non-POST requests to the managed tunnel route instead of rendering the app', async ({ request }) => { + test.skip(tunnelRouteMode !== 'static', 'Requires a known managed tunnel path'); + + const response = await request.get('/monitor'); + + expect(response.status()).toBe(405); + expect(response.headers()['allow']).toBe('POST'); +}); + function pathnameMatchesTunnelRoute(pathname: string): boolean { return typeof expectedTunnelPathMatcher === 'string' ? pathname === expectedTunnelPathMatcher diff --git a/packages/tanstackstart-react/src/vite/tunnelRoute.ts b/packages/tanstackstart-react/src/vite/tunnelRoute.ts index c7af98412461..06f3337b4e5e 100644 --- a/packages/tanstackstart-react/src/vite/tunnelRoute.ts +++ b/packages/tanstackstart-react/src/vite/tunnelRoute.ts @@ -194,6 +194,10 @@ export const Route = createFileRoute(${serializedTunnelRoute})({ const Sentry = await import('@sentry/tanstackstart-react'); return Sentry.createSentryTunnelRoute(${serializedAllowedDsns ? `{ allowedDsns: ${serializedAllowedDsns} }` : `{}`}).handlers.POST({ request }); }, + // Without a handler for a method, TanStack Start falls back to SSR-rendering the app (running root loaders). + ANY() { + return new Response(null, { status: 405, headers: { Allow: 'POST' } }); + }, }, }, }); diff --git a/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts b/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts index 822a01aeeeff..22b3a9be56a5 100644 --- a/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts +++ b/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts @@ -139,6 +139,15 @@ describe('tunnelRoute vite plugin', () => { expect(virtualRouteModule).toContain('createSentryTunnelRoute({})'); }); + it('rejects non-POST requests in the virtual managed tunnel route module', async () => { + const plugin = makeTunnelRoutePlugin('/monitor'); + + const virtualRouteModule = plugin.load && (await plugin.load('\0virtual:sentry-tanstackstart-react/tunnel-route')); + + expect(virtualRouteModule).toContain('ANY()'); + expect(virtualRouteModule).toContain("status: 405, headers: { Allow: 'POST' }"); + }); + it('treats an empty string `path` like omitted and uses a generated tunnel route', () => { const plugin = makeTunnelRoutePlugin({ path: '' });