diff --git a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts index ce54e8e25f85..fbbe8f704fbe 100644 --- a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts +++ b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts @@ -2,6 +2,7 @@ import { SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions'; import { getClient, GLOBAL_OBJ, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, type Span, type SpanAttributes } from '@sentry/core'; import { isSentryRequestSpan } from '@sentry/opentelemetry'; import { ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes'; +import { isPathnameUnderSentryTunnelRoute } from './tunnelPathnameMatch'; import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached'; const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { @@ -59,7 +60,7 @@ function isTunnelRouteSpan(spanAttributes: Record): boolean { // Extract pathname from the target (e.g., "/tunnel?o=123&p=456" -> "/tunnel") const pathname = httpTarget.split('?')[0] || ''; - return pathname === tunnelPath || pathname.startsWith(`${tunnelPath}/`); + return isPathnameUnderSentryTunnelRoute(pathname, tunnelPath); } return false; diff --git a/packages/nextjs/src/common/utils/tunnelPathnameMatch.ts b/packages/nextjs/src/common/utils/tunnelPathnameMatch.ts new file mode 100644 index 000000000000..9f107d33636c --- /dev/null +++ b/packages/nextjs/src/common/utils/tunnelPathnameMatch.ts @@ -0,0 +1,8 @@ +/** + * Returns true when `pathname` is exactly the Sentry tunnel route or a sub-path + * (`tunnelPath` + `/...`). A plain `startsWith(tunnelPath)` is unsafe: e.g. tunnel + * `/api/t` must not match `/api/things`. + */ +export function isPathnameUnderSentryTunnelRoute(pathname: string, tunnelPath: string): boolean { + return pathname === tunnelPath || pathname.startsWith(`${tunnelPath}/`); +} diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts index 985354543a0d..d383837cbf17 100644 --- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts +++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts @@ -13,6 +13,7 @@ import { withIsolationScope, } from '@sentry/core'; import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd'; +import { isPathnameUnderSentryTunnelRoute } from '../common/utils/tunnelPathnameMatch'; import type { EdgeRouteHandler } from '../edge/types'; /** @@ -36,7 +37,7 @@ export function wrapMiddlewareWithSentry( // Check if the current request matches the tunnel route if (req instanceof Request) { const url = new URL(req.url); - const isTunnelRequest = url.pathname.startsWith(tunnelRoute); + const isTunnelRequest = isPathnameUnderSentryTunnelRoute(url.pathname, tunnelRoute); if (isTunnelRequest) { // Create a simple response that mimics NextResponse.next() so we don't need to import internals here diff --git a/packages/nextjs/test/config/wrappers.test.ts b/packages/nextjs/test/config/wrappers.test.ts index c61c92026f60..7d5f4029bd94 100644 --- a/packages/nextjs/test/config/wrappers.test.ts +++ b/packages/nextjs/test/config/wrappers.test.ts @@ -193,4 +193,33 @@ describe('wrapMiddlewareWithSentry', () => { 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 tunnel sub-paths under tunnelRoute', async () => { + (globalThis as any)._sentryRewritesTunnelPath = '/api/t'; + + const origFunction: EdgeRouteHandler = vi.fn(async () => ({ status: 200 })); + const wrappedOriginal = wrapMiddlewareWithSentry(origFunction); + + const mockRequest = new Request('https://example.com/api/t/envelope?o=1'); + + const result = await wrappedOriginal(mockRequest); + + expect(origFunction).not.toHaveBeenCalled(); + expect(result).toBeDefined(); + }); });