Skip to content

Commit ef27f39

Browse files
committed
ref(nextjs): Forward tunnel route requests with core's handleTunnelRequest
The tunnel route handler now leaves DSN normalization, the allowlist and tracing suppression to core. As in every other SDK, `allowedDsns` adds to the server's own DSN, and the default `POST` accepts the DSNs `serveTunnelIntegration` allows.
1 parent de10e52 commit ef27f39

3 files changed

Lines changed: 39 additions & 57 deletions

File tree

‎packages/nextjs/src/config/types.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,8 @@ export type SentryBuildOptions = Omit<
280280
* export { POST } from '@sentry/nextjs/tunnel';
281281
* ```
282282
*
283-
* This handler accepts the DSN of the server SDK, also a self-hosted one. For other DSNs, use
284-
* `createSentryTunnelRoute` from the same import.
283+
* This handler accepts the DSN of the server SDK, also a self-hosted one, and the DSNs `serveTunnelIntegration`
284+
* allows. For other DSNs, use `createSentryTunnelRoute` from the same import.
285285
*
286286
* Tunnel requests go through your middleware (`proxy.ts` / `middleware.ts`) like any other request. If your
287287
* middleware redirects or blocks unauthenticated requests, exclude the tunnel route in its `matcher` so events
Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { dsnToString, getClient, handleTunnelRequest, makeDsn, suppressTracing } from '@sentry/core';
1+
import { handleTunnelRequest } from '@sentry/core';
22

33
export interface CreateSentryTunnelRouteOptions {
44
/**
5-
* The DSNs that the route accepts. It rejects envelopes for other DSNs with a 403.
6-
* Defaults to the DSN of the server SDK.
5+
* DSNs whose envelopes the route forwards, in addition to the DSN of the server SDK. It rejects envelopes for other
6+
* DSNs with a 403. Defaults to the DSNs `serveTunnelIntegration` allows.
77
*/
88
allowedDsns?: string[];
99
}
@@ -27,32 +27,19 @@ type TunnelRouteHandler = (request: Request) => Promise<Response>;
2727
* ```
2828
*/
2929
export function createSentryTunnelRoute(options: CreateSentryTunnelRouteOptions = {}): { POST: TunnelRouteHandler } {
30-
const allowedDsnsFromOptions = options.allowedDsns?.length ? normalizeDsns(options.allowedDsns) : undefined;
30+
const allowedDsns = options.allowedDsns?.length ? options.allowedDsns.map(dsn => dsn.trim()) : undefined;
3131

3232
return {
33-
POST: async request => {
34-
// Read on each request, because the client can be set up after this module loads.
35-
const allowedDsns = allowedDsnsFromOptions ?? getServerDsns();
36-
if (!allowedDsns.length) {
37-
return new Response(
38-
allowedDsnsFromOptions
39-
? 'Tunnel route has no valid DSN in allowedDsns.'
40-
: 'Tunnel route requires Sentry server SDK initialized with a DSN, or pass allowedDsns explicitly.',
41-
{ status: 500 },
42-
);
43-
}
44-
45-
// Like the SDK transports, so that the forward creates no spans, breadcrumbs or trace headers.
46-
return suppressTracing(() => handleTunnelRequest(request, { allowedDsns }));
47-
},
33+
POST: request => handleTunnelRequest(request, { allowedDsns }),
4834
};
4935
}
5036

5137
/**
5238
* Route handler for the `tunnelRoute` of `withSentryConfig`. It forwards Sentry envelopes to Sentry, but not the
5339
* request headers (such as cookies).
5440
*
55-
* It accepts envelopes only for the DSN of the server SDK. For other DSNs, use `createSentryTunnelRoute`.
41+
* It accepts envelopes for the DSN of the server SDK, and the DSNs `serveTunnelIntegration` allows. For other DSNs,
42+
* use `createSentryTunnelRoute`.
5643
*
5744
* @example
5845
* ```ts
@@ -61,16 +48,3 @@ export function createSentryTunnelRoute(options: CreateSentryTunnelRouteOptions
6148
* ```
6249
*/
6350
export const POST: TunnelRouteHandler = createSentryTunnelRoute().POST;
64-
65-
function getServerDsns(): string[] {
66-
const dsn = getClient()?.getDsn();
67-
return dsn ? [dsnToString(dsn)] : [];
68-
}
69-
70-
// The same form as the DSN in the envelope header, because `handleTunnelRequest` compares exact strings.
71-
function normalizeDsns(dsns: string[]): string[] {
72-
return dsns.flatMap(dsn => {
73-
const dsnComponents = makeDsn(dsn.trim());
74-
return dsnComponents ? [dsnToString(dsnComponents)] : [];
75-
});
76-
}

‎packages/nextjs/test/tunnel/index.test.ts‎

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
import type * as SentryCore from '@sentry/core';
2-
import { createEnvelope, makeDsn, serializeEnvelope } from '@sentry/core';
1+
import type { Client } from '@sentry/core';
2+
import { createEnvelope, getCurrentScope, makeDsn, serializeEnvelope } from '@sentry/core';
33
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4+
import { serveTunnelIntegration } from '@sentry/core/server';
45
import { createSentryTunnelRoute, POST } from '../../src/tunnel';
56

6-
const { mockGetClient } = vi.hoisted(() => ({ mockGetClient: vi.fn() }));
7-
8-
vi.mock('@sentry/core', async importOriginal => ({
9-
...(await importOriginal<typeof SentryCore>()),
10-
getClient: mockGetClient,
11-
}));
12-
137
const SERVER_DSN = 'https://public@o1.ingest.sentry.io/1';
148
const BROWSER_DSN = 'https://browser@o1.ingest.sentry.io/2';
159

@@ -33,7 +27,8 @@ function createEnvelopeRequest(dsn: string): { request: Request; body: Uint8Arra
3327
}
3428

3529
function useClientDsn(dsn: string | undefined): void {
36-
mockGetClient.mockReturnValue(dsn ? { getDsn: () => makeDsn(dsn) } : undefined);
30+
const client = dsn ? ({ getDsn: () => makeDsn(dsn), getOptions: () => ({}) } as unknown as Client) : undefined;
31+
getCurrentScope().setClient(client);
3732
}
3833

3934
let fetchSpy: ReturnType<typeof vi.spyOn>;
@@ -75,9 +70,6 @@ describe('POST', () => {
7570
const response = await POST(request);
7671

7772
expect(response.status).toBe(500);
78-
await expect(response.text()).resolves.toBe(
79-
'Tunnel route requires Sentry server SDK initialized with a DSN, or pass allowedDsns explicitly.',
80-
);
8173
expect(fetchSpy).not.toHaveBeenCalled();
8274
});
8375

@@ -91,6 +83,22 @@ describe('POST', () => {
9183
expect(fetchSpy).not.toHaveBeenCalled();
9284
});
9385

86+
it('accepts the DSNs `serveTunnelIntegration` allows', async () => {
87+
const integration = serveTunnelIntegration({ allowedDsns: [BROWSER_DSN] });
88+
const options = {};
89+
const client = {
90+
getDsn: () => makeDsn(SERVER_DSN),
91+
getOptions: () => options,
92+
getIntegrationByName: () => integration,
93+
} as unknown as Client;
94+
integration.setup?.(client);
95+
getCurrentScope().setClient(client);
96+
97+
const response = await POST(createEnvelopeRequest(BROWSER_DSN).request);
98+
99+
expect(response.status).toBe(200);
100+
});
101+
94102
it('does not accept DSNs from environment variables', async () => {
95103
useClientDsn(SERVER_DSN);
96104
vi.stubEnv('SENTRY_DSN', BROWSER_DSN);
@@ -104,16 +112,18 @@ describe('POST', () => {
104112
});
105113

106114
describe('createSentryTunnelRoute', () => {
107-
it('accepts only the DSNs in `allowedDsns`', async () => {
115+
it('accepts the DSNs in `allowedDsns` and the server DSN', async () => {
108116
useClientDsn(SERVER_DSN);
109117
const { POST: handler } = createSentryTunnelRoute({ allowedDsns: [BROWSER_DSN] });
110118

111-
const allowedResponse = await handler(createEnvelopeRequest(BROWSER_DSN).request);
112-
const rejectedResponse = await handler(createEnvelopeRequest(SERVER_DSN).request);
119+
const browserResponse = await handler(createEnvelopeRequest(BROWSER_DSN).request);
120+
const serverResponse = await handler(createEnvelopeRequest(SERVER_DSN).request);
121+
const otherResponse = await handler(createEnvelopeRequest('https://other@o2.ingest.sentry.io/3').request);
113122

114-
expect(allowedResponse.status).toBe(200);
115-
expect(rejectedResponse.status).toBe(403);
116-
expect(fetchSpy).toHaveBeenCalledTimes(1);
123+
expect(browserResponse.status).toBe(200);
124+
expect(serverResponse.status).toBe(200);
125+
expect(otherResponse.status).toBe(403);
126+
expect(fetchSpy).toHaveBeenCalledTimes(2);
117127
});
118128

119129
it.each([
@@ -138,15 +148,13 @@ describe('createSentryTunnelRoute', () => {
138148
expect(response.status).toBe(200);
139149
});
140150

141-
it('responds with 500 when no allowed DSN is valid', async () => {
151+
it('responds with 500 when no allowed DSN is valid and the server SDK has no DSN', async () => {
142152
vi.spyOn(console, 'error').mockImplementation(() => undefined);
143-
useClientDsn(SERVER_DSN);
144153
const { POST: handler } = createSentryTunnelRoute({ allowedDsns: ['not-a-dsn'] });
145154

146155
const response = await handler(createEnvelopeRequest(SERVER_DSN).request);
147156

148157
expect(response.status).toBe(500);
149-
await expect(response.text()).resolves.toBe('Tunnel route has no valid DSN in allowedDsns.');
150158
expect(fetchSpy).not.toHaveBeenCalled();
151159
});
152160
});

0 commit comments

Comments
 (0)