Skip to content

Commit 2911b4a

Browse files
authored
feat(nextjs): Automatically skip middleware requests for tunnel route (#16812)
1 parent fa6138b commit 2911b4a

File tree

3 files changed

+133
-6
lines changed

3 files changed

+133
-6
lines changed

packages/nextjs/src/common/wrapMiddlewareWithSentry.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,31 @@ export function wrapMiddlewareWithSentry<H extends EdgeRouteHandler>(
2727
): (...params: Parameters<H>) => Promise<ReturnType<H>> {
2828
return new Proxy(middleware, {
2929
apply: async (wrappingTarget, thisArg, args: Parameters<H>) => {
30+
const tunnelRoute =
31+
'_sentryRewritesTunnelPath' in globalThis
32+
? (globalThis as Record<string, unknown>)._sentryRewritesTunnelPath
33+
: undefined;
34+
35+
if (tunnelRoute && typeof tunnelRoute === 'string') {
36+
const req: unknown = args[0];
37+
// Check if the current request matches the tunnel route
38+
if (req instanceof Request) {
39+
const url = new URL(req.url);
40+
const isTunnelRequest = url.pathname.startsWith(tunnelRoute);
41+
42+
if (isTunnelRequest) {
43+
// Create a simple response that mimics NextResponse.next() so we don't need to import internals here
44+
// which breaks next 13 apps
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+
}
53+
}
54+
}
3055
// TODO: We still should add central isolation scope creation for when our build-time instrumentation does not work anymore with turbopack.
3156
return withIsolationScope(isolationScope => {
3257
const req: unknown = args[0];

packages/nextjs/src/config/withSentryConfig.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,17 +107,15 @@ function getFinalConfigObject(
107107
showedExportModeTunnelWarning = true;
108108
// eslint-disable-next-line no-console
109109
console.warn(
110-
'[@sentry/nextjs] The Sentry Next.js SDK `tunnelRoute` option will not work in combination with Next.js static exports. The `tunnelRoute` option uses serverside features that cannot be accessed in export mode. If you still want to tunnel Sentry events, set up your own tunnel: https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option',
110+
'[@sentry/nextjs] The Sentry Next.js SDK `tunnelRoute` option will not work in combination with Next.js static exports. The `tunnelRoute` option uses server-side features that cannot be accessed in export mode. If you still want to tunnel Sentry events, set up your own tunnel: https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option',
111111
);
112112
}
113113
} else {
114114
const resolvedTunnelRoute =
115-
typeof userSentryOptions.tunnelRoute === 'boolean'
116-
? generateRandomTunnelRoute()
117-
: userSentryOptions.tunnelRoute;
115+
userSentryOptions.tunnelRoute === true ? generateRandomTunnelRoute() : userSentryOptions.tunnelRoute;
118116

119117
// Update the global options object to use the resolved value everywhere
120-
userSentryOptions.tunnelRoute = resolvedTunnelRoute;
118+
userSentryOptions.tunnelRoute = resolvedTunnelRoute || undefined;
121119
setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute);
122120
}
123121
}

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

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import type { Client } from '@sentry/core';
22
import * as SentryCore from '@sentry/core';
33
import type { IncomingMessage, ServerResponse } from 'http';
44
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
5-
import { wrapGetInitialPropsWithSentry, wrapGetServerSidePropsWithSentry } from '../../src/common';
5+
import {
6+
wrapGetInitialPropsWithSentry,
7+
wrapGetServerSidePropsWithSentry,
8+
wrapMiddlewareWithSentry,
9+
} from '../../src/common';
10+
import type { EdgeRouteHandler } from '../../src/edge/types';
611

712
const startSpanManualSpy = vi.spyOn(SentryCore, 'startSpanManual');
813

@@ -84,3 +89,102 @@ describe('data-fetching function wrappers should not create manual spans', () =>
8489
expect(mockSetAttribute).not.toHaveBeenCalled();
8590
});
8691
});
92+
93+
describe('wrapMiddlewareWithSentry', () => {
94+
afterEach(() => {
95+
vi.clearAllMocks();
96+
if ('_sentryRewritesTunnelPath' in globalThis) {
97+
delete (globalThis as any)._sentryRewritesTunnelPath;
98+
}
99+
});
100+
101+
test('should skip processing and return NextResponse.next() for tunnel route requests', async () => {
102+
// Set up tunnel route in global
103+
(globalThis as any)._sentryRewritesTunnelPath = '/monitoring/tunnel';
104+
105+
const origFunction: EdgeRouteHandler = vi.fn(async () => ({ status: 200 }));
106+
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
107+
108+
// Create a mock Request that matches the tunnel route
109+
const mockRequest = new Request('https://example.com/monitoring/tunnel?o=123');
110+
111+
const result = await wrappedOriginal(mockRequest);
112+
113+
// Should skip calling the original function
114+
expect(origFunction).not.toHaveBeenCalled();
115+
expect(result).toBeDefined();
116+
});
117+
118+
test('should process normal request and call original function', async () => {
119+
const mockReturnValue = { status: 200 };
120+
const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
121+
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
122+
123+
const mockRequest = new Request('https://example.com/api/users', { method: 'GET' });
124+
125+
const result = await wrappedOriginal(mockRequest);
126+
127+
expect(origFunction).toHaveBeenCalledWith(mockRequest);
128+
expect(result).toBe(mockReturnValue);
129+
});
130+
131+
test('should handle non-Request arguments', async () => {
132+
const mockReturnValue = { status: 200 };
133+
const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
134+
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
135+
136+
const mockArgs = { someProperty: 'value' };
137+
138+
const result = await wrappedOriginal(mockArgs);
139+
140+
expect(origFunction).toHaveBeenCalledWith(mockArgs);
141+
expect(result).toBe(mockReturnValue);
142+
});
143+
144+
test('should handle errors in middleware function', async () => {
145+
const testError = new Error('Test middleware error');
146+
const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => {
147+
throw testError;
148+
});
149+
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
150+
151+
const mockRequest = new Request('https://example.com/api/users');
152+
153+
await expect(wrappedOriginal(mockRequest)).rejects.toThrow('Test middleware error');
154+
expect(origFunction).toHaveBeenCalledWith(mockRequest);
155+
});
156+
157+
test('should not process tunnel route when no tunnel path is set', async () => {
158+
if ('_sentryRewritesTunnelPath' in globalThis) {
159+
delete (globalThis as any)._sentryRewritesTunnelPath;
160+
}
161+
162+
const mockReturnValue = { status: 200 };
163+
const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
164+
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
165+
166+
const mockRequest = new Request('https://example.com/monitoring/tunnel/sentry?o=123');
167+
168+
const result = await wrappedOriginal(mockRequest);
169+
170+
// Should process normally since no tunnel path is configured
171+
expect(origFunction).toHaveBeenCalledWith(mockRequest);
172+
expect(result).toBe(mockReturnValue);
173+
});
174+
175+
test('should process request when tunnel path is set but request does not match', async () => {
176+
(globalThis as any)._sentryRewritesTunnelPath = '/monitoring/tunnel';
177+
178+
const mockReturnValue = { status: 200 };
179+
const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
180+
const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
181+
182+
const mockRequest = new Request('https://example.com/api/users', { method: 'GET' });
183+
184+
const result = await wrappedOriginal(mockRequest);
185+
186+
// Should process normally since request doesn't match tunnel path
187+
expect(origFunction).toHaveBeenCalledWith(mockRequest);
188+
expect(result).toBe(mockReturnValue);
189+
});
190+
});

0 commit comments

Comments
 (0)