As Oleg pointed out, the error handling using AsyncTransactionalExecutionStrategy
is broken inside nested transactions.
Since the URL in his answer does not work anymore, here is how I have solved it.
First lets have some exception I want to properly handle through GraphQL response
public class UserFriendlyException extends RuntimeException {
public UserFriendlyException(String message) {
super(message);
}
}
Then I defined error response
public class UserFriendlyGraphQLError implements GraphQLError {
/** Message shown to user */
private final String message;
private final List<SourceLocation> locations;
private final ExecutionPath path;
public UserFriendlyGraphQLError(String message, List<SourceLocation> locations, ExecutionPath path) {
this.message = message;
this.locations = locations;
this.path = path;
}
@Override
public String getMessage() {
return message;
}
@Override
public List<SourceLocation> getLocations() {
return locations;
}
@Override
public ErrorClassification getErrorType() {
return CustomErrorClassification.USER_FRIENDLY_ERROR;
}
@Override
public List<Object> getPath() {
return path.toList();
}
}
public enum CustomErrorClassification implements ErrorClassification {
USER_FRIENDLY_ERROR
}
Then I created DataFetcherExceptionHandler
to transform it into proper GraphQL response
/**
* Converts exceptions into error response
*/
public class GraphQLExceptionHandler implements DataFetcherExceptionHandler {
private final DataFetcherExceptionHandler delegate = new SimpleDataFetcherExceptionHandler();
@Override
public DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) {
// handle user friendly errors
if (handlerParameters.getException() instanceof UserFriendlyException) {
GraphQLError error = new UserFriendlyGraphQLError(
handlerParameters.getException().getMessage(),
List.of(handlerParameters.getSourceLocation()),
handlerParameters.getPath());
return DataFetcherExceptionHandlerResult.newResult().error(error).build();
}
// delegate to default handler otherwise
return delegate.onException(handlerParameters);
}
}
And finally used it in the AsyncTransactionalExecutionStrategy
, using also the @Transactional
annotation to allow lazy resolvers
@Component
public class AsyncTransactionalExecutionStrategy extends AsyncExecutionStrategy {
public AsyncTransactionalExecutionStrategy() {
super(new GraphQLExceptionHandler());
}
@Override
@Transactional
public CompletableFuture<ExecutionResult> execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException {
return super.execute(executionContext, parameters);
}
}
Now if you throw new UserFriendlyException("Email already exists");
somewhere you would end up with nice response like
{
"errors": [
{
"message": "Email already exists",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createUser"
],
"extensions": {
"classification": "USER_FRIENDLY_ERROR"
}
}
],
"data": null
}
Given the classification USER_FRIENDLY_ERROR
you can directly show it to the user, if you made UserFriendlyException
messages user-friendly :)
However if you throw new UserFriendlyException("Email already exists");
inside some method annotated with @Transactional
you end up with empty response and HTTP 400 status.
Adding @Transactional(propagation = Propagation.REQUIRES_NEW)
to Mutation
solves this issue
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class Mutation implements GraphQLMutationResolver {
public User createUser(...) {
...
}
}
Note that this is probably not so performant solution. However it could suffice for some smaller projects.
competitionRepository.findByShowId(show.getId())
. Is this the only way you could access the competition collection from the show entity without eager loading? – Papistry