Skip to content

Commit dc7b1f5

Browse files
authored
fix(node): Fix nest.js error handler (#11874)
This was brought up here #5578 (comment), our error handler implementation was too naive, and our tests not ideal - the tests only checked that stuff is sent to sentry (which it was!) but not that the page otherwise worked. Now, I updated the test to ensure this works as expected. With this PR, the signature for `Sentry.setupNestErrorHandler()` changes ( breaking change, but sadly required at this point). You have to pass in an exception filter, which we'll extend to _also_ send exceptions to Sentry: ```js import { BaseExceptionFilter, HttpAdapterHost } from '@nestjs/core'; const { httpAdapter } = app1.get(HttpAdapterHost); Sentry.setupNestErrorHandler(app1, new BaseExceptionFilter(httpAdapter)); ``` This is a bit more involved, but also allows you to use a custom filter if needed (we can extend _any_ exception filter).
1 parent 1c4e776 commit dc7b1f5

File tree

6 files changed

+42
-20
lines changed

6 files changed

+42
-20
lines changed

dev-packages/e2e-tests/test-applications/node-nestjs-app/src/main.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { NestFactory } from '@nestjs/core';
1+
import { BaseExceptionFilter, HttpAdapterHost, NestFactory } from '@nestjs/core';
22
import * as Sentry from '@sentry/node';
33
import { AppModule1, AppModule2 } from './app.module';
44

@@ -15,7 +15,9 @@ async function bootstrap() {
1515
});
1616

1717
const app1 = await NestFactory.create(AppModule1);
18-
Sentry.setupNestErrorHandler(app1);
18+
19+
const { httpAdapter } = app1.get(HttpAdapterHost);
20+
Sentry.setupNestErrorHandler(app1, new BaseExceptionFilter(httpAdapter));
1921

2022
await app1.listen(app1Port);
2123

dev-packages/e2e-tests/test-applications/node-nestjs-app/tests/errors.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,12 @@ test('Sends exception to Sentry', async ({ baseURL }) => {
4747
});
4848

4949
try {
50-
axios.get(`${baseURL}/test-exception/123`);
51-
} catch {
52-
// this results in an error, but we don't care - we want to check the error event
50+
await axios.get(`${baseURL}/test-exception/123`);
51+
// Should never be reached!
52+
expect(false).toBe(true);
53+
} catch (error) {
54+
expect(error).toBeInstanceOf(AxiosError);
55+
expect(error.response?.status).toBe(500);
5356
}
5457

5558
const errorEvent = await errorEventPromise;

dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/scenario.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Sentry.init({
1515
});
1616

1717
import { Controller, Get, Injectable, Module, Param } from '@nestjs/common';
18-
import { NestFactory } from '@nestjs/core';
18+
import { BaseExceptionFilter, HttpAdapterHost, NestFactory } from '@nestjs/core';
1919

2020
const port = 3480;
2121

@@ -49,7 +49,8 @@ class AppModule {}
4949

5050
async function init(): Promise<void> {
5151
const app = await NestFactory.create(AppModule);
52-
Sentry.setupNestErrorHandler(app);
52+
const { httpAdapter } = app.get(HttpAdapterHost);
53+
Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter));
5354
await app.listen(port);
5455
sendPortToRunner(port);
5556
}

dev-packages/node-integration-tests/suites/tracing/nestjs-errors/scenario.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Sentry.init({
1313
});
1414

1515
import { Controller, Get, Injectable, Module, Param } from '@nestjs/common';
16-
import { NestFactory } from '@nestjs/core';
16+
import { BaseExceptionFilter, HttpAdapterHost, NestFactory } from '@nestjs/core';
1717

1818
const port = 3460;
1919

@@ -47,7 +47,8 @@ class AppModule {}
4747

4848
async function init(): Promise<void> {
4949
const app = await NestFactory.create(AppModule);
50-
Sentry.setupNestErrorHandler(app);
50+
const { httpAdapter } = app.get(HttpAdapterHost);
51+
Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter));
5152
await app.listen(port);
5253
sendPortToRunner(port);
5354
}

dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/scenario.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Sentry.init({
1515
});
1616

1717
import { Controller, Get, Injectable, Module, Param } from '@nestjs/common';
18-
import { NestFactory } from '@nestjs/core';
18+
import { BaseExceptionFilter, HttpAdapterHost, NestFactory } from '@nestjs/core';
1919

2020
const port = 3470;
2121

@@ -49,7 +49,8 @@ class AppModule {}
4949

5050
async function init(): Promise<void> {
5151
const app = await NestFactory.create(AppModule);
52-
Sentry.setupNestErrorHandler(app);
52+
const { httpAdapter } = app.get(HttpAdapterHost);
53+
Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter));
5354
await app.listen(port);
5455
sendPortToRunner(port);
5556
}

packages/node/src/integrations/tracing/nest.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ interface MinimalNestJsExecutionContext {
1717
};
1818
};
1919
}
20+
21+
interface NestJsErrorFilter {
22+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
23+
catch(exception: any, host: any): void;
24+
}
25+
2026
interface MinimalNestJsApp {
21-
useGlobalFilters: (arg0: { catch(exception: unknown): void }) => void;
27+
useGlobalFilters: (arg0: NestJsErrorFilter) => void;
2228
useGlobalInterceptors: (interceptor: {
2329
intercept: (context: MinimalNestJsExecutionContext, next: { handle: () => void }) => void;
2430
}) => void;
@@ -40,16 +46,10 @@ const _nestIntegration = (() => {
4046
*/
4147
export const nestIntegration = defineIntegration(_nestIntegration);
4248

43-
const SentryNestExceptionFilter = {
44-
catch(exception: unknown) {
45-
captureException(exception);
46-
},
47-
};
48-
4949
/**
5050
* Setup an error handler for Nest.
5151
*/
52-
export function setupNestErrorHandler(app: MinimalNestJsApp): void {
52+
export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsErrorFilter): void {
5353
app.useGlobalInterceptors({
5454
intercept(context, next) {
5555
if (getIsolationScope() === getDefaultIsolationScope()) {
@@ -65,5 +65,19 @@ export function setupNestErrorHandler(app: MinimalNestJsApp): void {
6565
},
6666
});
6767

68-
app.useGlobalFilters(SentryNestExceptionFilter);
68+
const wrappedFilter = new Proxy(baseFilter, {
69+
get(target, prop, receiver) {
70+
if (prop === 'catch') {
71+
const originalCatch = Reflect.get(target, prop, receiver);
72+
73+
return (exception: unknown, host: unknown) => {
74+
captureException(exception);
75+
return originalCatch.apply(target, [exception, host]);
76+
};
77+
}
78+
return Reflect.get(target, prop, receiver);
79+
},
80+
});
81+
82+
app.useGlobalFilters(wrappedFilter);
6983
}

0 commit comments

Comments
 (0)