From 5df0122022f5e62117b0bed87d6f9fcd10ecb05d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 22 Sep 2026 13:35:04 +0200 Subject: [PATCH 1/2] feat(nextjs)!: Remove middleware bypass for tunnel route requests In webpack builds, the middleware wrapper returned early for requests to the tunnel route so the user's middleware never ran for them. Turbopack builds never had this behavior, and keeping the SDK's request matching in sync with the tunnel rewrite is fragile. Tunnel requests now go through the user's middleware like any other request. Users whose middleware blocks unauthenticated requests need to exclude the tunnel route in their matcher. Refs JS-3719 Co-Authored-By: Claude Fable 5.1 --- .../src/common/utils/tunnelPathnameMatch.ts | 25 ------ .../src/common/wrapMiddlewareWithSentry.ts | 21 ----- packages/nextjs/src/config/types.ts | 4 + packages/nextjs/test/config/wrappers.test.ts | 89 +------------------ 4 files changed, 6 insertions(+), 133 deletions(-) diff --git a/packages/nextjs/src/common/utils/tunnelPathnameMatch.ts b/packages/nextjs/src/common/utils/tunnelPathnameMatch.ts index ce8cacd0d264..9f107d33636c 100644 --- a/packages/nextjs/src/common/utils/tunnelPathnameMatch.ts +++ b/packages/nextjs/src/common/utils/tunnelPathnameMatch.ts @@ -6,28 +6,3 @@ export function isPathnameUnderSentryTunnelRoute(pathname: string, tunnelPath: string): boolean { return pathname === tunnelPath || pathname.startsWith(`${tunnelPath}/`); } - -/** - * Returns true only for requests the tunnel rewrite (see `setUpTunnelRewriteRules`) would serve. - * - * This decides whether the user's middleware is skipped, so it must never be broader than the rewrite: - * anything it matches that Next.js does not rewrite to Sentry reaches the app without middleware. - */ -export function isSentryTunnelRequest(request: Request, tunnelPath: string): boolean { - // The SDK transport only ever sends POST requests - if (request.method !== 'POST') { - return false; - } - - const url = new URL(request.url); - - if (url.pathname !== tunnelPath && url.pathname !== `${tunnelPath}/`) { - return false; - } - - // Next.js evaluates `has` conditions against the last value of a repeated query param, so every value has to qualify - return ['o', 'p'].every(key => { - const values = url.searchParams.getAll(key); - return values.length > 0 && values.every(value => /^\d+$/.test(value)); - }); -} diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts index c9367433b123..fb1c56565a4d 100644 --- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts +++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts @@ -9,7 +9,6 @@ import { withIsolationScope, } from '@sentry/core'; import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd'; -import { isSentryTunnelRequest } from '../common/utils/tunnelPathnameMatch'; import type { EdgeRouteHandler } from '../edge/types'; /** @@ -27,26 +26,6 @@ export function wrapMiddlewareWithSentry( ): (...params: Parameters) => Promise> { return new Proxy(middleware, { apply: async (wrappingTarget, thisArg, args: Parameters) => { - const tunnelRoute = - '_sentryRewritesTunnelPath' in globalThis - ? (globalThis as Record)._sentryRewritesTunnelPath - : undefined; - - // TODO: This can never work with Turbopack, need to remove it for consistency between builds. - if (tunnelRoute && typeof tunnelRoute === 'string') { - const req: unknown = args[0]; - if (req instanceof Request && isSentryTunnelRequest(req, tunnelRoute)) { - // Create a simple response that mimics NextResponse.next() so we don't need to import Next.js internals here - // https://github.com/vercel/next.js/blob/c12c9c1f78ad384270902f0890dc4cd341408105/packages/next/src/server/web/spec-extension/response.ts#L146 - return new Response(null, { - status: 200, - headers: { - 'x-middleware-next': '1', - }, - }) as ReturnType; - } - } - // TODO: We still should add central isolation scope creation for when our build-time instrumentation does not work anymore with turbopack. return withIsolationScope(isolationScope => { const req: unknown = args[0]; diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 6750f2d43fff..2ad05c994433 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -268,6 +268,10 @@ export type SentryBuildOptions = Omit< * - Pass `true` to auto-generate a random, ad-blocker-resistant route for each build * - Pass a string path (e.g., '/monitoring') to use a custom route * + * Tunnel requests go through your middleware (`proxy.ts` / `middleware.ts`) like any other request. If your + * middleware redirects or blocks unauthenticated requests, exclude the tunnel route in its `matcher` so events + * can reach Sentry. Matchers have to be static, so use a fixed string route in that case rather than `true`. + * * NOTE: This feature only works with Next.js 11+ */ tunnelRoute?: string | boolean; diff --git a/packages/nextjs/test/config/wrappers.test.ts b/packages/nextjs/test/config/wrappers.test.ts index e2059853ce96..9d121172eea9 100644 --- a/packages/nextjs/test/config/wrappers.test.ts +++ b/packages/nextjs/test/config/wrappers.test.ts @@ -104,23 +104,6 @@ describe('wrapMiddlewareWithSentry', () => { } }); - test('should skip processing and return NextResponse.next() for tunnel route requests', async () => { - // Set up tunnel route in global - (globalThis as any)._sentryRewritesTunnelPath = '/monitoring/tunnel'; - - const origFunction: EdgeRouteHandler = vi.fn(async () => ({ status: 200 })); - const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); - - // Create a mock Request that matches the tunnel route - const mockRequest = new Request('https://example.com/monitoring/tunnel?o=123&p=456', { method: 'POST' }); - - const result = await wrappedOriginal(mockRequest); - - // Should skip calling the original function - expect(origFunction).not.toHaveBeenCalled(); - expect(result).toBeDefined(); - }); - test('should process normal request and call original function', async () => { const mockReturnValue = { status: 200 }; const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue); @@ -160,82 +143,14 @@ describe('wrapMiddlewareWithSentry', () => { expect(origFunction).toHaveBeenCalledWith(mockRequest); }); - test('should not process tunnel route when no tunnel path is set', async () => { - if ('_sentryRewritesTunnelPath' in globalThis) { - delete (globalThis as any)._sentryRewritesTunnelPath; - } - - const mockReturnValue = { status: 200 }; - const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue); - const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); - - const mockRequest = new Request('https://example.com/monitoring/tunnel/sentry?o=123'); - - const result = await wrappedOriginal(mockRequest); - - // Should process normally since no tunnel path is configured - expect(origFunction).toHaveBeenCalledWith(mockRequest); - expect(result).toBe(mockReturnValue); - }); - - test('should process request when tunnel path is set but request does not match', async () => { - (globalThis as any)._sentryRewritesTunnelPath = '/monitoring/tunnel'; - - const mockReturnValue = { status: 200 }; - const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue); - const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); - - const mockRequest = new Request('https://example.com/api/users', { method: 'GET' }); - - const result = await wrappedOriginal(mockRequest); - - // Should process normally since request doesn't match tunnel path - expect(origFunction).toHaveBeenCalledWith(mockRequest); - expect(result).toBe(mockReturnValue); - }); - - test('should not treat paths as tunnel when they only share a prefix with tunnelRoute', async () => { - (globalThis as any)._sentryRewritesTunnelPath = '/api/t'; - - const mockReturnValue = { status: 200 }; - const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue); - const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); - - const mockRequest = new Request('https://example.com/api/things', { method: 'GET' }); - - const result = await wrappedOriginal(mockRequest); - - expect(origFunction).toHaveBeenCalledWith(mockRequest); - expect(result).toBe(mockReturnValue); - }); - - test('should skip processing for the tunnel route with a trailing slash', async () => { - (globalThis as any)._sentryRewritesTunnelPath = '/monitoring'; - - const origFunction: EdgeRouteHandler = vi.fn(async () => ({ status: 200 })); - const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); - - await wrappedOriginal(new Request('https://example.com/monitoring/?o=123&p=456&r=us', { method: 'POST' })); - - expect(origFunction).not.toHaveBeenCalled(); - }); - - test.each([ - ['a sub-path of the tunnel route', 'https://example.com/monitoring/anything/at/all?o=123&p=456', 'POST'], - ['a tunnel request without query params', 'https://example.com/monitoring', 'POST'], - ['a tunnel request without project id', 'https://example.com/monitoring?o=123', 'POST'], - ['a tunnel request with non-numeric ids', 'https://example.com/monitoring?o=abc&p=456', 'POST'], - ['a tunnel request with a repeated non-numeric org id', 'https://example.com/monitoring?o=123&o=abc&p=456', 'POST'], - ['a tunnel request with a repeated empty project id', 'https://example.com/monitoring?o=123&p=456&p=', 'POST'], - ['a non-POST tunnel request', 'https://example.com/monitoring?o=123&p=456', 'GET'], - ])('should run the middleware for %s', async (_, url, method) => { + test('should run the middleware for requests to the tunnel route', async () => { (globalThis as any)._sentryRewritesTunnelPath = '/monitoring'; const mockReturnValue = { status: 200 }; const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue); const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); - const mockRequest = new Request(url, { method }); + const mockRequest = new Request('https://example.com/monitoring?o=123&p=456', { method: 'POST' }); const result = await wrappedOriginal(mockRequest); From 9b7220c84e88ddc2306b7c73cffd57461ddddb9d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 22 Sep 2026 14:14:30 +0200 Subject: [PATCH 2/2] docs(nextjs): Add migration note for tunnel route middleware change Co-Authored-By: Claude Fable 5.1 --- MIGRATION.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MIGRATION.md b/MIGRATION.md index f059cd041035..80630a915f83 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1232,6 +1232,8 @@ Because the integration owns error capture, `setupFastifyErrorHandler` no longer **Tracing removed from generated templates:** Tracing was removed from the generated Pages Router API handler, Edge API handler, and Middleware wrapper templates. Route handlers and middleware are still instrumented automatically, so no action is required for most users. +**`tunnelRoute` requests now run through your middleware:** Webpack builds no longer skip your middleware for tunnel route requests. If your middleware blocks unauthenticated requests globally, exclude the tunnel route in its `matcher`, which requires a fixed string `tunnelRoute` instead of `true`. + **Unified `reactComponentAnnotation` option:** React component annotation is now configured through a single top-level `reactComponentAnnotation` option that applies to both webpack and Turbopack builds: ```js