Skip to content

Commit edd1867

Browse files
authored
fix(nextjs): Ensure we do not match tunnel endpoints too broadly (#20488)
This PR ensures we use the same matching logic for nextjs tunnel routes consistently.
1 parent 3bb1722 commit edd1867

4 files changed

Lines changed: 41 additions & 2 deletions

File tree

packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions';
22
import { getClient, GLOBAL_OBJ, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, type Span, type SpanAttributes } from '@sentry/core';
33
import { isSentryRequestSpan } from '@sentry/opentelemetry';
44
import { ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes';
5+
import { isPathnameUnderSentryTunnelRoute } from './tunnelPathnameMatch';
56
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached';
67

78
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
@@ -59,7 +60,7 @@ function isTunnelRouteSpan(spanAttributes: Record<string, unknown>): boolean {
5960
// Extract pathname from the target (e.g., "/tunnel?o=123&p=456" -> "/tunnel")
6061
const pathname = httpTarget.split('?')[0] || '';
6162

62-
return pathname === tunnelPath || pathname.startsWith(`${tunnelPath}/`);
63+
return isPathnameUnderSentryTunnelRoute(pathname, tunnelPath);
6364
}
6465

6566
return false;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Returns true when `pathname` is exactly the Sentry tunnel route or a sub-path
3+
* (`tunnelPath` + `/...`). A plain `startsWith(tunnelPath)` is unsafe: e.g. tunnel
4+
* `/api/t` must not match `/api/things`.
5+
*/
6+
export function isPathnameUnderSentryTunnelRoute(pathname: string, tunnelPath: string): boolean {
7+
return pathname === tunnelPath || pathname.startsWith(`${tunnelPath}/`);
8+
}

packages/nextjs/src/common/wrapMiddlewareWithSentry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
withIsolationScope,
1414
} from '@sentry/core';
1515
import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd';
16+
import { isPathnameUnderSentryTunnelRoute } from '../common/utils/tunnelPathnameMatch';
1617
import type { EdgeRouteHandler } from '../edge/types';
1718

1819
/**
@@ -36,7 +37,7 @@ export function wrapMiddlewareWithSentry<H extends EdgeRouteHandler>(
3637
// Check if the current request matches the tunnel route
3738
if (req instanceof Request) {
3839
const url = new URL(req.url);
39-
const isTunnelRequest = url.pathname.startsWith(tunnelRoute);
40+
const isTunnelRequest = isPathnameUnderSentryTunnelRoute(url.pathname, tunnelRoute);
4041

4142
if (isTunnelRequest) {
4243
// Create a simple response that mimics NextResponse.next() so we don't need to import internals here

packages/nextjs/test/config/wrappers.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,4 +193,33 @@ describe('wrapMiddlewareWithSentry', () => {
193193
expect(origFunction).toHaveBeenCalledWith(mockRequest);
194194
expect(result).toBe(mockReturnValue);
195195
});
196+
197+
test('should not treat paths as tunnel when they only share a prefix with tunnelRoute', async () => {
198+
(globalThis as any)._sentryRewritesTunnelPath = '/api/t';
199+
200+
const mockReturnValue = { status: 200 };
201+
const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
202+
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
203+
204+
const mockRequest = new Request('https://example.com/api/things', { method: 'GET' });
205+
206+
const result = await wrappedOriginal(mockRequest);
207+
208+
expect(origFunction).toHaveBeenCalledWith(mockRequest);
209+
expect(result).toBe(mockReturnValue);
210+
});
211+
212+
test('should skip processing for tunnel sub-paths under tunnelRoute', async () => {
213+
(globalThis as any)._sentryRewritesTunnelPath = '/api/t';
214+
215+
const origFunction: EdgeRouteHandler = vi.fn(async () => ({ status: 200 }));
216+
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
217+
218+
const mockRequest = new Request('https://example.com/api/t/envelope?o=1');
219+
220+
const result = await wrappedOriginal(mockRequest);
221+
222+
expect(origFunction).not.toHaveBeenCalled();
223+
expect(result).toBeDefined();
224+
});
196225
});

0 commit comments

Comments
 (0)