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

feat(@nestjs/graphql): base exception filter for apollo exceptions #1292

Merged
merged 1 commit into from
Mar 19, 2021

Conversation

nirga
Copy link
Contributor

@nirga nirga commented Dec 17, 2020

Closes #1053

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

[X] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Other... Please describe:

What is the current behavior?

Exceptions are built by Apollo server engine, and because it doesn't recognizes Nest.js's exceptions, it always returns a generic HTTP 500 error, even for other errors, specifically UNAUTHORIZED.

Issue Number: #1053

What is the new behavior?

Exceptions are converted explicitly to an ApolloError, possibly to a specific exception (if exists), and then thrown for the Apollo server to catch and format.

Does this PR introduce a breaking change?

[X] Yes
[ ] No

This change might cause breakages if clients used to assume HTTP 500 is always returned. However, this will now be in-line with the rest of Apollo server errors and exceptions so it should work well with compliant clients.

Other information

@@ -46,6 +47,7 @@ import {
GraphQLTypesLoader,
GraphQLSchemaBuilder,
GraphQLSchemaHost,
{ provide: APP_FILTER, useClass: GraphQLExceptionFilter },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will bind the filter for the entire application. If someone is using GQL and REST they are now using the GraphQLExceptionFilter for both contexts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a check in GraphQLExceptionFilter to rethrow exceptions that were thrown not in GraphQL context.
If you think it make sense, I can go ahead and add a test for that specifically. I'd probably need to add a REST controller to the code-first test app, does it make sense?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid introducing breaking changes, let's make this optional for now and inform users that they have to explicitly register this filter in their applications. Later (in v8), we can get back to this discussion and think about registering it automatically.

@Catch(HttpException)
export class GraphQLExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
if (host.getType<GqlContextType>() != 'graphql') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (host.getType<GqlContextType>() != 'graphql') {
if (host.getType<GqlContextType>() !== 'graphql') {

Comment on lines 7 to 11
import {
ApolloError,
AuthenticationError,
ForbiddenError,
} from 'apollo-server-express';
Copy link
Member

@kamilmysliwiec kamilmysliwiec Dec 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't import from the 'apollo-server-express' as this will break Fastify based applications. Can you please import from base/core packages (not sure which one exposes these error classes)?

@kamilmysliwiec
Copy link
Member

Thanks for your contribution! I've left a few comments/suggestions

export class ApolloExceptionFilter implements GqlExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
if (host.getType<GqlContextType>() !== 'graphql') {
throw exception;
Copy link

@faboulaws faboulaws Dec 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok to throw an exception here? From what I understand it not ok

The ExceptionFilter is always the last place that gets called before a response is sent out, it is responsible for building the response. You cannot rethrow an exception from within an ExceptionFilter.

also

Note that exceptions cannot be passed from filter to filter

Since this PR was not merged I copied this class to our own project. Our project has both a graphQL and REST routes. This line is crashing the App. I had to make the following change to make it work

 if (host.getType<GqlContextType>() !== 'graphql') {
      const ctx = host.switchToHttp();
      const response = ctx.getResponse<Response>();
      const status = exception.getStatus();

      response
        .status(status)
        .json({
          statusCode: status,
          message: exception.message
        });
      return exception;
    }

I am still new to NestJS. SO am I missing something here?

Copy link
Contributor Author

@nirga nirga Dec 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, thanks for flagging! That's weird though, I'd expect that if an error is not handled by a certain exception filter, it will be "caught" by the next one. Not sure if that's on purpose or not but it seems that there's a difference between how ExternalExceptionsHandler is handling exceptions (where it falls back to the default behavior if the exception filter returned a falsy value), and the http exception handler in ExceptionsHandler (where it falls back to the default behavior only if there was no exception filter found). @kamilmysliwiec WDYT?

@@ -0,0 +1,39 @@
import { Catch, ArgumentsHost, HttpException } from '@nestjs/common';
Copy link

@faboulaws faboulaws Dec 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if this class is needed. This feels more like an example on how to implement exception filters for GraphQL. Most people will not use this directly since custom logic such as logging etc might be required depending on use case. Maybe as a base class similar to BaseExceptionFilter.

Another approach is to define a helper to convert HTTPExceptions to Apollo Errors.

import { ApolloError, AuthenticationError, ForbiddenError } from 'apollo-server-errors'
import { HttpException } from '@nestjs/common'

const apolloPredefinedExceptions: Record<number, typeof ApolloError> = {
  401: AuthenticationError,
  403: ForbiddenError
}

export function convertErrorToApolloError(exception: HttpException): ApolloError {
  let error: ApolloError
  if (exception.getStatus() in apolloPredefinedExceptions) {
    error = new apolloPredefinedExceptions[exception.getStatus()](
      exception.message
    )
  } else {
    error = new ApolloError(
      exception.message,
      exception.getStatus().toString()
    )
  }

  error.stack = exception.stack
  error.extensions['response'] = exception.getResponse()
  return error
}

Then this helper can be used in any custom ExceptionFilter implementing GqlExceptionFilter like below.

@Catch(HttpException)
export class MyCustomExceptionFilter implements GqlExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
   const apolloError: ApolloError = convertErrorToApolloError(exception)

    this.logger.error(apolloError)
    return apolloError;
  }
}

Just my humble opinion 😃. I might be wrong here. Still new to NestJS.

Copy link
Contributor Author

@nirga nirga Dec 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this class should be as part of the infra so you get this behavior "out of the box" without needing to worry about the internals of Nest exceptions vs. Apollo exceptions. And this is indeed the plan (see @kamilmysliwiec comment above), but I'm changing its name to be GraphQLBaseExceptionFilter for now so it'll be clear that you should inherit from it in GraphQL environments.

error.stack = exception.stack;
error.extensions['response'] = exception.getResponse();

throw error;
Copy link

@faboulaws faboulaws Dec 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the GqlExceptionFilter interface the error should be returned here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's on purpose though. I want ApolloServer to catch this exception so it can format it nicely in the response.

@nirga nirga force-pushed the exceptions branch 2 times, most recently from 8a5c8b3 to 865514c Compare December 27, 2020 09:53
@nirga nirga requested a review from kamilmysliwiec December 27, 2020 09:54
@nirga nirga changed the title fix(@nestjs/graphql): Convert Nest exceptions to Apollo exceptions fix(@nestjs/graphql): base exception filter for converting nest exceptions to apollo exceptions Dec 27, 2020
@nirga nirga changed the title fix(@nestjs/graphql): base exception filter for converting nest exceptions to apollo exceptions feat(@nestjs/graphql): base exception filter for converting nest exceptions to apollo exceptions Dec 27, 2020
@nirga nirga changed the title feat(@nestjs/graphql): base exception filter for converting nest exceptions to apollo exceptions feat(@nestjs/graphql): base exception filter for apollo exceptions Dec 27, 2020
} from 'apollo-server-errors';
import { GqlContextType } from './services';

const apolloPredefinedExceptions: Record<number, typeof ApolloError> = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why UserInputError and other ApolloErrors are not included?

@incompletude
Copy link

Is there a timeline for this PR? I need it really bad.

@kamilmysliwiec kamilmysliwiec changed the base branch from master to 8.0.0 March 19, 2021 10:28
@kamilmysliwiec kamilmysliwiec merged commit e98e966 into nestjs:8.0.0 Mar 19, 2021
@kimroen
Copy link

kimroen commented Apr 16, 2021

To avoid introducing breaking changes, let's make this optional for now and inform users that they have to explicitly register this filter in their applications. Later (in v8), we can get back to this discussion and think about registering it automatically.

@kamilmysliwiec Based on this comment it looks like this was slated for a 7.x release, but due to the recent base change and merge into the nestjs:8.0.0 branch, I'm assuming this is no longer the case.

Would that be accurate? Just want to make sure I'm not just missing this landing in a 7.x release, I'm totally fine with doing workarounds until 8 🙂

@kamilmysliwiec
Copy link
Member

Correct @kimroen

@viztor
Copy link

viztor commented Jun 11, 2021

Correct @kimroen

May I suggest v7 to include GraphQLBaseExceptionFilter for developers to register? That could smooth the upgrade to v8 later. It is suggested in the comment that it would be done like that but it seems it's not there yet.

@ScreamZ
Copy link

ScreamZ commented Aug 4, 2021

Had issue tracking where this code belongs to in v8 now, in order to understand its behavior, for those who get interested in like me just check :

function wrapFormatErrorFn(options: GqlModuleOptions) {

@abouroubi
Copy link

Looks like this was removed from version 9.
Is there a new way to handle exception in graphql now ?

@JVMartin
Copy link

JVMartin commented Mar 3, 2022

I'm confused... in v8, do I need to register a GraphQLExceptionFilter like this?

app.useGlobalFilters(GraphQLExceptionFilter);

I can't find this exported anywhere, so I'm not sure how to set this up. Trying to throw Apollo exceptions in v8 graphql, and they are logged as errors when they shouldn't be.

@kasparszarinovs
Copy link

Hi there. I was wondering if there's any movement on this, or similar solutions? Or is there any other go-to way to format GQL errors, except for setting exceptionFactory manually and having a manual formatter on GraphQL module?

@Distortedlogic
Copy link

Can we get some docs on the built-in nestjs patterns for handling graphql expection filtering? When I google, I dont get much thats informative or get things that are old. And I cannot find anything on doing it for subscriptions.. Trial and error to figure this out isnt preferred and means I potentially miss nestjs stuff that would simplify things.

@oonsamyi
Copy link

@nirga @kamilmysliwiec Any updates, please?
How to use GraphQLBaseExceptionFilter?

@dijonkitchen
Copy link
Contributor

@nestjs nestjs locked and limited conversation to collaborators Jun 14, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

GraphQL error handling