Skip to content

Annotated exception handler methods #160

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

Closed
Sam-Kruglov opened this issue Oct 4, 2021 · 7 comments
Closed

Annotated exception handler methods #160

Sam-Kruglov opened this issue Oct 4, 2021 · 7 comments
Assignees
Labels
in: core Issues related to config and core support type: enhancement A general enhancement
Milestone

Comments

@Sam-Kruglov
Copy link

Sam-Kruglov commented Oct 4, 2021

Hi, would be a nice addition to support @ExceptionHandler methods and @ControllerAdvice. Those could be injected into the exception resolver chain. This feature is available here (at least to some extend) https://github.com/graphql-java-kickstart/graphql-spring-boot

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Oct 4, 2021
@bclozel
Copy link
Member

bclozel commented Oct 6, 2021

Thanks for raising this point @Sam-Kruglov

We can mix and match MVC, WebFlux and GraphQL handler methods in the same @Controller class - but our engine can sort them out because they're using different annotations: @RequestMapping, @SchemaMapping and their variants.

It looks like graphql-java-kickstart accepts the following signatures for @ExceptionHandler annotated methods:

  • List<GraphQLError> handle(MyException exc)
  • ThrowableGraphQLError handle(MyException exc)

I'm surprised the choice was done here to support only synchronous calls.

Right now @ExceptionHandler methods accept a wide variety of arguments and return types.

With that in mind, I think that supporting @ExceptionHandler can cause issues for several reasons:

  1. the @ExceptionHandler method signatures is really flexible and we're likely to run into conflicts
  2. developers might think that somehow a single @ExceptionHandler method could be used for both MVC and GraphQL, which doesn't seem likely or even possible
  3. @ExceptionHandler in itself is confusing, since we're never handling an exception but rather translating an "internal" exception into user-facing GraphQL errors
  4. The @ExceptionHandler contract can be localized to a controller or @ControllerAdvice and called only when a close matching handler was in charge of processing the request. With Spring GraphQL, the DataFetcherExceptionResolver is global as exceptions can happen at any point during the data fetching process

Maybe we could instead with an @ExceptionResolver contract supporting the following.

Method Argument Description
Exception Type Exception raised during data fetching
DataFetchingEnvironment Environment for the invoked DataFetcher
GraphqlErrorBuilder New GraphqlErrorBuilder instance built with the current environment
Return Value Description
GraphQLError, List<GraphQLError> Error(s) to add to the response
Mono<GraphQLError>, Flux<GraphQLError> Error(s) to add to the response
null Indicates that the Exception remains unresolved

As we can see, dealing with reactive types for return values can lead to subtle behaviors: the exception remains unresolved vs. no error should be added to the response. The org.springframework.graphql.execution.DataFetcherExceptionResolver#resolveException contract is quite clear about that, but developer expectations might not be the same for annotated methods.

@bclozel bclozel added the for: team-attention An issue we need to discuss as a team to make progress label Oct 6, 2021
@Sam-Kruglov
Copy link
Author

Sam-Kruglov commented Oct 6, 2021

Yeah, you're right, I like @ExceptionResolver suggestion!

Also, GraphQL can return errors and data at the same time, so when an error occurs, it occurs inside a field resolver (is it called data fetcher here?). So, when a resolver fails, the exception translates via this annotation method, and gets added to the "errors" field of the graphql response. Other resolvers should be able to proceed their work and populate the "data" field of the graphql response. Not sure if this is the current behaviour already but from the documentation I got the impression that a single error would fail everything.

@bclozel
Copy link
Member

bclozel commented Oct 6, 2021

Also, GraphQL can return errors and data at the same time, so when an error occurs, it occurs inside a field resolver (is it called data fetcher here?).

Yes, that's right. In Spring GraphQL, we're using GraphQL Java concepts such as DataFetcher.

So, when a resolver fails, the exception translates via this annotation method, and gets added to the "errors" field of the graphql response. Other resolvers should be able to proceed their work and populate the "data" field of the graphql response.

Yes, this is the current behavior - and IMO expected behavior for GraphQL applications.

@bclozel bclozel added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged for: team-attention An issue we need to discuss as a team to make progress labels Nov 25, 2021
@bclozel bclozel added this to the 1.0 Backlog milestone Nov 25, 2021
@rstoyanchev rstoyanchev changed the title @ExceptionHandler support @ExceptionResolver support Nov 25, 2021
@rstoyanchev
Copy link
Contributor

The underlying mechanism is still that of GraphQL Java, so when a controller raises an exception, it has to propagate at first. Then GraphQL Java calls the registered DataFetcherExceptionHandler and we can check which controller raised the exception, look for @ExceptionResolver methods, go through @ControllerAdvice, etc.

What we have currently, as explained in the docs, is that you can register one or more DataFetcherExceptionResolver beans which apply globally like a ControllerAdvice. For example:

@Bean
public DataFetcherExceptionResolver exceptionResolver() {
	return DataFetcherExceptionResolverAdapter.from((ex, env) -> {
		if (ex instanceof SomeException) {
			return GraphqlErrorBuilder.newError(env).message("...").errorType(...).build();
		}
		else if (ex instanceof AnotherException) {
			return GraphqlErrorBuilder.newError(env).message("...").errorType(...).build();
		}
		else {
			return null;
		}
	});
}

I'm wondering whether this is sufficient for GraphQL? For once the inputs are simpler and for output there is no need to write to a response, which reduces the amount of benefit from such methods. At the same time there is extra overhead in GraphQL where there may be multiple exceptions per request for different parts of the data graph and the exception resolution steps are repeated multiple times. The other aspect could be the need for localized handling. It would be useful to hear more feedback on all this.

@Sam-Kruglov
Copy link
Author

Sam-Kruglov commented Nov 25, 2021

@rstoyanchev I think it's just nicer to have separate @ExceptionResolver methods instead of a single method with a bunch of if (ex instanceof .... Other than that, DataFetcherExceptionResolver seems sufficient. But why can we supply multiple instances of it if it can handle everything?

Some exceptions will be in response for the client to see, some will be internal errors that the client should not see but we still want to recover from or at least log.

@rstoyanchev rstoyanchev added the in: core Issues related to config and core support label Nov 25, 2021
@hanrw
Copy link

hanrw commented Jun 10, 2022

It's good way than using if (ex instanceof ....

@ExceptionHandler(
      EntityNotFoundException::class,
      NoSuchElementException::class,
      EmptyResultDataAccessException::class,
      IndexOutOfBoundsException::class,
      KotlinNullPointerException::class
  )
  fun notFoundException(e: Exception): GraphQLError {
      return GraphqlErrorBuilder.newError().message(e.message).build()
  }

@rstoyanchev
Copy link
Contributor

rstoyanchev commented Mar 10, 2023

It looks like the DataFetcherExceptionResolver exposed by AnnotatedControllerConfigurer does not yield correctly to other resolvers when there is no suitable annotated exception handler to handle the exception. More details in spring-projects/spring-boot#34526 (comment).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues related to config and core support type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

5 participants