-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NestJs - setupNestErrorHandler causes exceptions filters to not being called, therefore errors are not being handled #12351
Comments
If I change Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter)); to: Sentry.setupNestErrorHandler(app, new DragonExceptionFilter(httpAdapter)); It will handle DragonModule exceptions and not HeroModule. One possible solution: Not pass second argument (BaseExceptionFilter) and automatically iterate over all filters that are registered in nest and apply proxying of exception filter to all of them |
I'm seeing this as well- I have a custom Exception Filter that catches authentication errors on routes with Basic Auth, and responds with a
I've found a non-ideal workaround, which is to register the filter globally in my main.ts file, after running Sentry.setupNestErrorHandler(
app,
new GlobalExceptionFilter(app.getHttpAdapter()),
);
// Sentry's exception filter prevents any module-defined filters from running, so register here.
// Must be _after_ Sentry.setupNestErrorHandler
app.useGlobalFilters(new BasicAuthErrorFilter()); |
So if you have a custom filter, you should wrap that filter instead of
Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter));
Sentry.setupNestErrorHandler(app, new DragonExceptionFilter(httpAdapter)); I guess we do not really support multiple filters today, but we could provide a utility that allows to wrap any error filter, and/or a utility to add sentry support to your own error filter - would that make sense to you? E.g. (this is theoretical, just how this could work): // Use this instead of setupNestErrorHandler()
Sentry.setupNestApp(app);
app.useGlobalFilters(wrapNestFilter(new BasicAuthErrorFilter()); |
It might make sense but in your solution there still is 1 problem and that is registration of filters using modules. Common practice is that you create exception filter per module and provide it in module's providers, something like this: // hero.module.ts
@Module({
providers: [
{ provide: APP_FILTER, useClass: HeroExceptionFilter }
],
})
export class HeroModule {}
// dragon.module.ts
@Module({
providers: [
{ provide: APP_FILTER, useClass: DragonExceptionFilter }
],
})
export class DragonModule {} Writing code like this does not require you to register exception filters in the bootstrap function. Better solution for me would be having SentryModule which will be configurable and you would just need to import it to main module and it will automatically catch the exceptionFilters and use proxy with new instance or change exception filter's prototype. Following code represents what I am saying (tested and it works well): async function bootstrap() {
const app = await NestFactory.create(AppModule);
Sentry.setupNestErrorHandler(app); // need to remove second argument and remove internal use of global prefixes
// New Code Start
const discoveryService = app.get(DiscoveryService);
const providers = discoveryService.getProviders();
const exceptionFilters = providers.filter(
({ metatype }) =>
metatype && Reflect.getMetadata(CATCH_WATERMARK, metatype),
);
exceptionFilters.map((mod) => {
const instance = mod.instance;
const originalCatch = instance.catch;
instance.catch = function (exception, host) {
Sentry.captureException(exception);
return originalCatch.apply(this, [exception, host]);
};
return mod;
});
// New Code End
await app.listen(3000);
} Here I am using DiscoveryService provided by nestjs (which is exported from DiscoveryModule), filtering out exceptionFilter providers and changing it's instance catch function. Maybe not the best practice but this would be nice to have. Extension and thing to do would be to create |
Thanks for elaborating! I think we could look into providing such a thing. I'll put this in the backlog for now, as we are still following up on some other issues, but we could provide some opt-in mechanism there. The challenge we have here is that we cannot (easily) depend on nestjs as proper dependency, so we need to pass in all things that come from nest. Also stuff like the watermark, for example 🤔 So could we provide an API like this: async function bootstrap() {
const app = await NestFactory.create(AppModule);
Sentry.setupNestErrorHandler(app); // need to remove second argument and remove internal use of global prefixes
const discoveryService = app.get(DiscoveryService);
const providers = discoveryService.getProviders();
const exceptionFilters = providers.filter(
({ metatype }) =>
metatype && Reflect.getMetadata(CATCH_WATERMARK, metatype),
);
Sentry.wrapNestExceptionFilters(exceptionFilters);
await app.listen(3000);
} would that make sense from your POV? |
For now to quickly fix this issue, this approach is acceptable for me :) |
Ok, thanks for the feedback! We'll look into this in the near future 🙏 |
Hey, I looked into this today. The issue seems to be resolvable by using UseFilters annotation on the controllers directly instead of injecting the exception filters in the module providers. Does that solution work for you? |
Same issue here. |
I agree, having to do UseFilters everywhere is too much work. We'll find a solution to make this nice to use and functional. We will update you here on the process. |
Hey everyone, We are about to merge a PR that allows to setup the Nest SDK by adding a root module: From the e2e tests we observed that this new setup also seems to resolve this issue. This will go out with the next release. Let us know if anything doesn't work as expected. |
- Adds a new nest root module that can be used to setup the Nest SDK as a replacement for the existing setup (with a function). Instead of calling `setupNestErrorHandler` in the main.ts file, users can now add `SentryModule.forRoot()` (feedback about the name is definitely welcome) as an import in their main app module. This approach is much more native to nest than what we used so far. This root module is introduced in the setup.ts file. - This root module is exported with a submodule export `@sentry/nestjs/setup`, because the SDK now depends on nestjs directly and without this the nest instrumentation does not work anymore, since nest gets imported before Sentry.init gets called, which disables the otel nest instrumentation. - Judging from the e2e tests it seems that this new approach also resolves some issues the previous implementation had, specifically [this issue](#12351) seems to be resolved. The e2e test that was in place, just documented the current (wrong) behavior. So I updated the test to reflect the new (correct) behavior. - I updated all the test applications to use the new approach but kept a copy of the nestjs-basic and nestjs-distributed-tracing with the old setup (now named node-nestjs-basic and node-nestjs-distributed-tracing respectively) so we can still verify that the old setup (which a lot of people use) still keeps working going forward. - Updated/New tests in this PR: - Sends unexpected exception to Sentry if thrown in Submodule - Does not send expected exception to Sentry if thrown in Submodule and caught by a global exception filter - Does not send expected exception to Sentry if thrown in Submodule and caught by a local exception filter - Sends expected exception to Sentry if thrown from submodule registered before Sentry - To accomodate the new tests I added several submodules in the nestjs-with-submodules test-application. These are overall similarly but have important distinctions: - example-module-local-filter: Submodule with a local filter registered using `@UseFilters` on the controller. - example-module-global-filter: Submodule with a global filter registered using APP_FILTER in the submodule definition. - example-module-global-filter-wrong-registration-order: Also has a global filter set with APP_FILTER, but is registered in the root module as first submodule, even before the SentryIntegration is initialized. This case does not work properly in the new setup (Sentry should be set first), so this module is used for tests documenting this behavior. - Also set "moduleResolution": "Node16" in the nestjs-basic sample app to ensure our submodule-export workaround works in both, default and sub-path-export-compatible TS configs as was suggested [here](#12948 (comment)).
Hey, Just updated to the last version of the package and used the new approach with SentryModule however, in my case, the issue is still persisting. I have two filters, one that catch BadRequestException, and a catch all. It works fine without registering SentryModule, but they are not called once I register it. My filters are registered using APP_FILTER in the main app module. Let me know if you need more context. |
Hey, thanks for writing in. Would you mind sharing a small reproduction sample? |
Sure, here are the main files. Main.ts import './instrument';
import { NestFactory } from '@nestjs/core';
import {
ExpressAdapter,
NestExpressApplication,
} from '@nestjs/platform-express';
import Express from 'express';
import { onRequest } from 'firebase-functions/v2/https';
import { ReplaySubject, firstValueFrom } from 'rxjs';
import { AppModule } from './app.module';
import { InternalDisabledLogger } from './shared/utils/logger';
const serverSubject = new ReplaySubject<Express.Express>();
const bootstrap = async (): Promise<any> => {
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
new ExpressAdapter(),
{
rawBody: true,
logger: new InternalDisabledLogger(),
bodyParser: true,
},
);
app.enableCors();
await app.init();
return app.getHttpAdapter().getInstance();
};
bootstrap().then((server) => serverSubject.next(server));
export const api = onRequest(
{
timeoutSeconds: 60 * 2,
memory: '4GiB',
cpu: 4,
region: 'europe-west1',
maxInstances: 10,
},
async (request, response) => {
const server = await firstValueFrom(serverSubject);
return server(request, response);
},
); Main app module @Module({
providers: [
{
provide: APP_FILTER,
useClass: GenericExceptionFilter,
},
{
provide: APP_FILTER,
useClass: BadRequestExceptionFilter,
},
{
provide: APP_INTERCEPTOR,
useClass: GenericResponseInterceptor,
},
],
imports: [
SentryModule.forRoot(),
// ... my other modules
],
})
export class AppModule {} Error filters @Catch(BadRequestException)
export class BadRequestExceptionFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost): void {
// custom logic, basically formatting the error response
}
}
@Catch()
export class GenericExceptionFilter implements ExceptionFilter {
catch(
exception:
| InternalServerErrorException
| ConflictException
| ForbiddenException
| ServiceException
| NotFoundException
| UnauthorizedException,
host: ArgumentsHost,
): void {
// custom logic, basically formatting the error response
}
} |
Thanks! I'll try to investigate this this week. Will keep you posted. |
Hey everyone, We are about to merge a PR that should resolve this issue. Be careful, we had to change the SDK setup a bit to make this work properly. The root module remains, but now you have to either add the The fix will go out with version |
…ted (#13278) [ref](#12351) This PR moves the `SentryGlobalFilter` out of the root module, which led to the filter overriding user exception filters in certain scenarios. Now there is two ways to setup sentry error monitoring: - If users have no catch-all exception filter in their application they add the `SentryGlobalFilter` as a provider in their main module. - If users have a catch-all exception filter (annotated with `@Catch()` they can use the newly introduced `SentryCaptureException()` decorator to capture alle exceptions caught by this filter. Testing: Added a new sample application to test the second setup option and expanded the test suite in general. Side note: - Also removed the `@sentry/microservices` dependency again, since it does not come out of the box with every nest application so we cannot rely on it.
Hey @nicohrubec - we are using GraphQL/NestJS and it looks like the newest version of the Sentry SDK for NestJS doesn't work for this. Specifically - I think it is somewhat related to this change regressing from #12128, or this comment #12128 (comment) It doesn't look like there is a way to specify a different base filter in the new version of the NestJS/Sentry SDK like in I'm wondering if
With the current impl - we are consistently getting
Let me know if you'd like me to open a new issue for this! Edit - I confirmed monkey-patching the
|
Hey @ysuhaas, thanks for writing in! If you could open an issue for this with the information above and the SDK version you are using that would be ideal. I'll have a look next week! |
I'm facing the same issue waiting for |
@rohitkhatri Hopefully today or tomorrow but definitely this week. |
…ted (getsentry#13278) [ref](getsentry#12351) This PR moves the `SentryGlobalFilter` out of the root module, which led to the filter overriding user exception filters in certain scenarios. Now there is two ways to setup sentry error monitoring: - If users have no catch-all exception filter in their application they add the `SentryGlobalFilter` as a provider in their main module. - If users have a catch-all exception filter (annotated with `@Catch()` they can use the newly introduced `SentryCaptureException()` decorator to capture alle exceptions caught by this filter. Testing: Added a new sample application to test the second setup option and expanded the test suite in general. Side note: - Also removed the `@sentry/microservices` dependency again, since it does not come out of the box with every nest application so we cannot rely on it.
Is there an existing issue for this?
How do you use Sentry?
Sentry Saas (sentry.io)
Which SDK are you using?
@sentry/node
SDK Version
8.7.0
Framework Version
10.0.0
Link to Sentry event
No response
SDK Setup
Steps to Reproduce
Reproducable github code: https://github.com/CSenshi/nest-sentry-exception-filter-bug
Expected Result
After integration exceptions should be handled/filtered as they were before
Actual Result
Errors are not being handled and return
{"statusCode":500,"message":"Internal server error"}
The text was updated successfully, but these errors were encountered: