-
Notifications
You must be signed in to change notification settings - Fork 399
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
Conversation
lib/graphql.module.ts
Outdated
@@ -46,6 +47,7 @@ import { | |||
GraphQLTypesLoader, | |||
GraphQLSchemaBuilder, | |||
GraphQLSchemaHost, | |||
{ provide: APP_FILTER, useClass: GraphQLExceptionFilter }, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
lib/graphql-exception.filter.ts
Outdated
@Catch(HttpException) | ||
export class GraphQLExceptionFilter implements ExceptionFilter { | ||
catch(exception: HttpException, host: ArgumentsHost) { | ||
if (host.getType<GqlContextType>() != 'graphql') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (host.getType<GqlContextType>() != 'graphql') { | |
if (host.getType<GqlContextType>() !== 'graphql') { |
lib/graphql-exception.filter.ts
Outdated
import { | ||
ApolloError, | ||
AuthenticationError, | ||
ForbiddenError, | ||
} from 'apollo-server-express'; |
There was a problem hiding this comment.
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)?
Thanks for your contribution! I've left a few comments/suggestions |
lib/apollo-exception.filter.ts
Outdated
export class ApolloExceptionFilter implements GqlExceptionFilter { | ||
catch(exception: HttpException, host: ArgumentsHost) { | ||
if (host.getType<GqlContextType>() !== 'graphql') { | ||
throw exception; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
lib/apollo-exception.filter.ts
Outdated
@@ -0,0 +1,39 @@ | |||
import { Catch, ArgumentsHost, HttpException } from '@nestjs/common'; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
lib/apollo-exception.filter.ts
Outdated
error.stack = exception.stack; | ||
error.extensions['response'] = exception.getResponse(); | ||
|
||
throw error; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
8a5c8b3
to
865514c
Compare
} from 'apollo-server-errors'; | ||
import { GqlContextType } from './services'; | ||
|
||
const apolloPredefinedExceptions: Record<number, typeof ApolloError> = { |
There was a problem hiding this comment.
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?
Is there a timeline for this PR? I need it really bad. |
@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 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 🙂 |
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. |
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 : graphql/lib/utils/merge-defaults.util.ts Line 78 in 83b4919
|
Looks like this was removed from version 9. |
I'm confused... in v8, do I need to register a GraphQLExceptionFilter like this?
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. |
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? |
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. |
@nirga @kamilmysliwiec Any updates, please? |
This might have moved to here: https://github.com/nestjs/graphql/blob/master/packages/graphql/lib/interfaces/gql-exception-filter.interface.ts |
Closes #1053
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
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?
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