Skip to content
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

Closed
3 tasks done
Tracked by #12504
CSenshi opened this issue Jun 4, 2024 · 20 comments · Fixed by #12920 or #13278
Closed
3 tasks done
Tracked by #12504
Assignees
Labels
Package: nestjs Issues related to the Sentry Nestjs SDK Package: node Issues related to the Sentry Node SDK Type: Bug

Comments

@CSenshi
Copy link

CSenshi commented Jun 4, 2024

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

import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';

// Ensure to call this before importing any other modules!
Sentry.init({
  dsn: 'https://[email protected]/0',
  integrations: [
    // Add our Profiling integration
    nodeProfilingIntegration(),
  ],

  // Add Performance Monitoring by setting tracesSampleRate
  // We recommend adjusting this value in production
  tracesSampleRate: 1.0,

  // Set sampling rate for profiling
  // This is relative to tracesSampleRate
  profilesSampleRate: 1.0,
});

Steps to Reproduce

  1. Add 2 separate modules with each having their own exception filter
  2. Integrate sentry as said in the docs (https://docs.sentry.io/platforms/javascript/guides/nestjs/)
  3. Old exceptions are not being handled

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"}

@github-actions github-actions bot added the Package: node Issues related to the Sentry Node SDK label Jun 4, 2024
@CSenshi
Copy link
Author

CSenshi commented Jun 4, 2024

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

@sambernard
Copy link

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 WWW-Authenticate header. I started getting the following response instead of a basic auth prompt:

{"message":"Unauthorized","statusCode":401}

I've found a non-ideal workaround, which is to register the filter globally in my main.ts file, after running Sentry.setupNestErrorHandler. Unfortunately this means that we lose automatic dependency injection on these filters.

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());

@mydea
Copy link
Member

mydea commented Jun 6, 2024

So if you have a custom filter, you should wrap that filter instead of new BaseExceptionFilter(httpAdapter). This is probably not documented ideally, we'll try to clarify this:

  1. If you had no custom error filter defined before, doing this should be fine:
Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter));
  1. If you had some other error handler defined before, wrap this instead, e.g.
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());

@CSenshi
Copy link
Author

CSenshi commented Jun 7, 2024

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 SentryModule which will have one service with onModuleInit function which will do the new code snippet automatically so user would only import SentryModule into main.module

@mydea
Copy link
Member

mydea commented Jun 10, 2024

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?

@CSenshi
Copy link
Author

CSenshi commented Jun 11, 2024

For now to quickly fix this issue, this approach is acceptable for me :)

@mydea
Copy link
Member

mydea commented Jun 12, 2024

Ok, thanks for the feedback! We'll look into this in the near future 🙏

@nicohrubec
Copy link
Contributor

nicohrubec commented Jul 2, 2024

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?

@mydea mydea added Package: nestjs Issues related to the Sentry Nestjs SDK and removed Integration: nest.js labels Jul 9, 2024
@SaizFerri
Copy link

Same issue here. UseFilters is a bit verbose if you have quite some filters.

@lforst
Copy link
Member

lforst commented Jul 11, 2024

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.

@nicohrubec
Copy link
Contributor

Hey everyone,

We are about to merge a PR that allows to setup the Nest SDK by adding a root module: SentryModule.forRoot()

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.

nicohrubec added a commit that referenced this issue Jul 23, 2024
- 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)).
@AymaneAit
Copy link

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.

@nicohrubec
Copy link
Contributor

Hey, thanks for writing in. Would you mind sharing a small reproduction sample?

@AymaneAit
Copy link

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
  }
}

@nicohrubec
Copy link
Contributor

Thanks! I'll try to investigate this this week. Will keep you posted.

@nicohrubec
Copy link
Contributor

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 SentryGlobalFilter to your app providers or decorate your existing global filters with the WithSentry decorator (check the README for specific instructions).

The fix will go out with version 8.27. If anything does not work as expected, let us know.

nicohrubec added a commit that referenced this issue Aug 13, 2024
…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.
@ysuhaas
Copy link

ysuhaas commented Aug 23, 2024

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 setupNestErrorHandler - which btw, should that be considered deprecated?

I'm wondering if SentryGlobalFilter needs to allow configuring a custom base filter here https://github.com/getsentry/sentry-javascript/blob/develop/packages/nestjs/src/setup.ts#L63 as you are able to do here

export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsErrorFilter): void {

With the current impl - we are consistently getting

{
      "query": "query {\n  triggerError\n}",
      "variables": {},
      "errors": [
        {
          "message": "response.status is not a function",
          "locations": [
            {
              "line": 2,
              "column": 3
            }
          ],
          "path": [
            "triggerError"
          ]
        }
      ]
    }

Let me know if you'd like me to open a new issue for this!

Edit - I confirmed monkey-patching the BaseExceptionFilter to ExternalExceptionFilter fixed the issue on GraphQL requests for us

diff --git a/build/cjs/setup.js b/build/cjs/setup.js
index 119735398bb49a01b1c941045a809edd45b32fdc..b06228c7d71288ef53f140de1890e9e4fefe7477 100644
--- a/build/cjs/setup.js
+++ b/build/cjs/setup.js
@@ -6,6 +6,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
 
 const common = require('@nestjs/common');
 const core = require('@nestjs/core');
+const filter = require('@nestjs/core/exceptions/external-exception-filter');
 const microservices = require('@nestjs/microservices');
 const core$1 = require('@sentry/core');
 const utils = require('@sentry/utils');
@@ -47,7 +48,7 @@ common.Injectable()(SentryTracingInterceptor);
 /**
  * Global filter to handle exceptions and report them to Sentry.
  */
-class SentryGlobalFilter extends core.BaseExceptionFilter {
+class SentryGlobalFilter extends filter.ExternalExceptionFilter {
    static  __initStatic2() {this.__SENTRY_INTERNAL__ = true;}
 
   /**

@nicohrubec
Copy link
Contributor

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!

@rohitkhatri
Copy link

I'm facing the same issue waiting for 8.27 release, any tentative time for this release?

@nicohrubec
Copy link
Contributor

@rohitkhatri Hopefully today or tomorrow but definitely this week.

Zen-cronic pushed a commit to Zen-cronic/sentry-javascript that referenced this issue Aug 26, 2024
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Package: nestjs Issues related to the Sentry Nestjs SDK Package: node Issues related to the Sentry Node SDK Type: Bug
Projects
Archived in project
10 participants