Skip to content

Commit 4bdcdeb

Browse files
chargomeclaude
andcommitted
fix(nextjs): Align tunnel request matching in middleware with tunnel rewrite
The middleware wrapper treated every path under the tunnel route as a tunnel request, while the tunnel rewrite only serves the exact route with the org and project query params. Match tunnel requests the same way the rewrite does so the two stay consistent. Fixes JS-3719 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent db6a395 commit 4bdcdeb

3 files changed

Lines changed: 61 additions & 22 deletions

File tree

‎packages/nextjs/src/common/utils/tunnelPathnameMatch.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,28 @@
66
export function isPathnameUnderSentryTunnelRoute(pathname: string, tunnelPath: string): boolean {
77
return pathname === tunnelPath || pathname.startsWith(`${tunnelPath}/`);
88
}
9+
10+
/**
11+
* Returns true only for requests the tunnel rewrite (see `setUpTunnelRewriteRules`) would serve.
12+
*
13+
* This decides whether the user's middleware is skipped, so it must never be broader than the rewrite:
14+
* anything it matches that Next.js does not rewrite to Sentry reaches the app without middleware.
15+
*/
16+
export function isSentryTunnelRequest(request: Request, tunnelPath: string): boolean {
17+
// The SDK transport only ever sends POST requests
18+
if (request.method !== 'POST') {
19+
return false;
20+
}
21+
22+
const url = new URL(request.url);
23+
24+
if (url.pathname !== tunnelPath && url.pathname !== `${tunnelPath}/`) {
25+
return false;
26+
}
27+
28+
// Next.js evaluates `has` conditions against the last value of a repeated query param, so every value has to qualify
29+
return ['o', 'p'].every(key => {
30+
const values = url.searchParams.getAll(key);
31+
return values.length > 0 && values.every(value => /^\d+$/.test(value));
32+
});
33+
}

‎packages/nextjs/src/common/wrapMiddlewareWithSentry.ts‎

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
withIsolationScope,
1010
} from '@sentry/core';
1111
import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd';
12-
import { isPathnameUnderSentryTunnelRoute } from '../common/utils/tunnelPathnameMatch';
12+
import { isSentryTunnelRequest } from '../common/utils/tunnelPathnameMatch';
1313
import type { EdgeRouteHandler } from '../edge/types';
1414

1515
/**
@@ -35,21 +35,15 @@ export function wrapMiddlewareWithSentry<H extends EdgeRouteHandler>(
3535
// TODO: This can never work with Turbopack, need to remove it for consistency between builds.
3636
if (tunnelRoute && typeof tunnelRoute === 'string') {
3737
const req: unknown = args[0];
38-
// Check if the current request matches the tunnel route
39-
if (req instanceof Request) {
40-
const url = new URL(req.url);
41-
const isTunnelRequest = isPathnameUnderSentryTunnelRoute(url.pathname, tunnelRoute);
42-
43-
if (isTunnelRequest) {
44-
// Create a simple response that mimics NextResponse.next() so we don't need to import Next.js internals here
45-
// https://github.com/vercel/next.js/blob/c12c9c1f78ad384270902f0890dc4cd341408105/packages/next/src/server/web/spec-extension/response.ts#L146
46-
return new Response(null, {
47-
status: 200,
48-
headers: {
49-
'x-middleware-next': '1',
50-
},
51-
}) as ReturnType<H>;
52-
}
38+
if (req instanceof Request && isSentryTunnelRequest(req, tunnelRoute)) {
39+
// Create a simple response that mimics NextResponse.next() so we don't need to import Next.js internals here
40+
// https://github.com/vercel/next.js/blob/c12c9c1f78ad384270902f0890dc4cd341408105/packages/next/src/server/web/spec-extension/response.ts#L146
41+
return new Response(null, {
42+
status: 200,
43+
headers: {
44+
'x-middleware-next': '1',
45+
},
46+
}) as ReturnType<H>;
5347
}
5448
}
5549

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

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ describe('wrapMiddlewareWithSentry', () => {
112112
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
113113

114114
// Create a mock Request that matches the tunnel route
115-
const mockRequest = new Request('https://example.com/monitoring/tunnel?o=123');
115+
const mockRequest = new Request('https://example.com/monitoring/tunnel?o=123&p=456', { method: 'POST' });
116116

117117
const result = await wrappedOriginal(mockRequest);
118118

@@ -209,17 +209,37 @@ describe('wrapMiddlewareWithSentry', () => {
209209
expect(result).toBe(mockReturnValue);
210210
});
211211

212-
test('should skip processing for tunnel sub-paths under tunnelRoute', async () => {
213-
(globalThis as any)._sentryRewritesTunnelPath = '/api/t';
212+
test('should skip processing for the tunnel route with a trailing slash', async () => {
213+
(globalThis as any)._sentryRewritesTunnelPath = '/monitoring';
214214

215215
const origFunction: EdgeRouteHandler = vi.fn(async () => ({ status: 200 }));
216216
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
217217

218-
const mockRequest = new Request('https://example.com/api/t/envelope?o=1');
218+
await wrappedOriginal(new Request('https://example.com/monitoring/?o=123&p=456&r=us', { method: 'POST' }));
219+
220+
expect(origFunction).not.toHaveBeenCalled();
221+
});
222+
223+
test.each([
224+
['a sub-path of the tunnel route', 'https://example.com/monitoring/anything/at/all?o=123&p=456', 'POST'],
225+
['a tunnel request without query params', 'https://example.com/monitoring', 'POST'],
226+
['a tunnel request without project id', 'https://example.com/monitoring?o=123', 'POST'],
227+
['a tunnel request with non-numeric ids', 'https://example.com/monitoring?o=abc&p=456', 'POST'],
228+
['a tunnel request with a repeated non-numeric org id', 'https://example.com/monitoring?o=123&o=abc&p=456', 'POST'],
229+
['a tunnel request with a repeated empty project id', 'https://example.com/monitoring?o=123&p=456&p=', 'POST'],
230+
['a non-POST tunnel request', 'https://example.com/monitoring?o=123&p=456', 'GET'],
231+
])('should run the middleware for %s', async (_, url, method) => {
232+
(globalThis as any)._sentryRewritesTunnelPath = '/monitoring';
233+
234+
const mockReturnValue = { status: 200 };
235+
const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
236+
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
237+
238+
const mockRequest = new Request(url, { method });
219239

220240
const result = await wrappedOriginal(mockRequest);
221241

222-
expect(origFunction).not.toHaveBeenCalled();
223-
expect(result).toBeDefined();
242+
expect(origFunction).toHaveBeenCalledWith(mockRequest);
243+
expect(result).toBe(mockReturnValue);
224244
});
225245
});

0 commit comments

Comments
 (0)