Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/nextjs/src/common/utils/tunnelPathnameMatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* 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));
});
}
27 changes: 11 additions & 16 deletions packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
withIsolationScope,
} from '@sentry/core';
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
import { isSentryTunnelRequest } from '../common/utils/tunnelPathnameMatch';
import type { EdgeRouteHandler } from '../edge/types';

/**
Expand All @@ -34,22 +35,16 @@ export function wrapMiddlewareWithSentry<H extends EdgeRouteHandler>(

if (tunnelRoute && typeof tunnelRoute === 'string') {
const req: unknown = args[0];
// 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);

if (isTunnelRequest) {
// Create a simple response that mimics NextResponse.next() so we don't need to import internals here
// which breaks next 13 apps
// 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<H>;
}
if (req instanceof Request && isSentryTunnelRequest(req, tunnelRoute)) {
// Create a simple response that mimics NextResponse.next() so we don't need to import internals here
// which breaks next 13 apps
// 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<H>;
}
}
// TODO: We still should add central isolation scope creation for when our build-time instrumentation does not work anymore with turbopack.
Expand Down
51 changes: 50 additions & 1 deletion packages/nextjs/test/config/wrappers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ describe('wrapMiddlewareWithSentry', () => {
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);

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

const result = await wrappedOriginal(mockRequest);

Expand Down Expand Up @@ -187,4 +187,53 @@ 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 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) => {
(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 result = await wrappedOriginal(mockRequest);

expect(origFunction).toHaveBeenCalledWith(mockRequest);
expect(result).toBe(mockReturnValue);
});
});
Loading