Reactive return type for DataFetcherExceptionResolver

Closes gh-52
This commit is contained in:
Rossen Stoyanchev
2021-05-19 15:04:02 +01:00
parent 827c188f63
commit a438d81404
4 changed files with 107 additions and 28 deletions

View File

@@ -19,29 +19,33 @@ import java.util.List;
import graphql.GraphQLError;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Mono;
/**
* Contract to resolve exceptions raised by {@link graphql.schema.DataFetcher}'s
* into errors to be added to the GraphQL response. Implementations are typically
* declared as beans in Spring configuration and invoked in order until one
* returns a non-null list of {@link GraphQLError}'s.
* to {@code GraphQLError}'s to add to the GraphQL response. Implementations are
* typically declared as beans in Spring configuration and invoked in order until
* one emits a List.
*
* <p>Use the {@link SingleErrorExceptionResolver} convenience adapter when you
* need to resolve exceptions to a single {@code GraphQLError} only.
*/
public interface DataFetcherExceptionResolver {
/**
* Resolve the given exception and return errors to add to the response.
* Resolve the given exception and return the error(s) to add to the response.
* <p>Implementations can use
* {@link graphql.GraphqlErrorBuilder#newError(DataFetchingEnvironment)} to
* create an error with the coordinates of the target field, and use
* {@link ErrorType} to specify a category for the error.
* @param exception the exception to resolve
* @param environment the environment for the invoked {@code DataFetcher}
* @return a (possibly empty) list of {@link GraphQLError}'s to add to the
* response, or {@code null} to indicate the exception is unresolved.
* @return a {@code Mono} with errors to add to the GraphQL response;
* if the {@code Mono} completes with an empty List, the exception is
* resolved without any errors added to the response;
* if the {@code Mono} completes empty, without emitting a List, the
* exception remains unresolved and gives other resolvers a chance.
*/
@Nullable
List<GraphQLError> resolveException(Throwable exception, DataFetchingEnvironment environment);
Mono<List<GraphQLError>> resolveException(Throwable exception, DataFetchingEnvironment environment);
}

View File

@@ -16,6 +16,7 @@
package org.springframework.graphql.execution;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletionException;
@@ -25,6 +26,9 @@ import graphql.execution.DataFetcherExceptionHandler;
import graphql.execution.DataFetcherExceptionHandlerParameters;
import graphql.execution.DataFetcherExceptionHandlerResult;
import graphql.schema.DataFetchingEnvironment;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import reactor.util.context.ContextView;
import org.springframework.util.Assert;
@@ -54,18 +58,22 @@ class ExceptionResolversExceptionHandler implements DataFetcherExceptionHandler
return invokeChain(exception, parameters.getDataFetchingEnvironment());
}
@SuppressWarnings("ConstantConditions")
public DataFetcherExceptionHandlerResult invokeChain(Throwable ex, DataFetchingEnvironment env) {
for (DataFetcherExceptionResolver resolver : this.resolvers) {
List<GraphQLError> errors = resolver.resolveException(ex, env);
if (errors != null) {
return DataFetcherExceptionHandlerResult.newResult().errors(errors).build();
}
}
GraphQLError error = applyDefaultHandling(ex, env);
return DataFetcherExceptionHandlerResult.newResult(error).build();
return Flux.fromIterable(this.resolvers)
.publishOn(Schedulers.boundedElastic()) // until GraphQL Java supports async exception handling
.flatMap(resolver -> resolver.resolveException(ex, env))
.next()
.defaultIfEmpty(Collections.singletonList(applyDefaultHandling(ex, env)))
.map(errors -> DataFetcherExceptionHandlerResult.newResult().errors(errors).build())
.contextWrite(context -> {
ContextView contextToAdd = ContextManager.getReactorContext(env);
return (contextToAdd != null ? context.putAll(contextToAdd) : context);
})
.block();
}
public GraphQLError applyDefaultHandling(Throwable ex, DataFetchingEnvironment env) {
private GraphQLError applyDefaultHandling(Throwable ex, DataFetchingEnvironment env) {
return GraphqlErrorBuilder.newError(env)
.message(ex.getMessage())
.errorType(ErrorType.INTERNAL_ERROR)

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.execution;
import java.util.Collections;
import java.util.List;
import graphql.GraphQLError;
import graphql.schema.DataFetchingEnvironment;
import reactor.core.publisher.Mono;
/**
* Simple adapter for {@link DataFetcherExceptionResolver} implementations that
* resolve exceptions to a single error only.
*/
public abstract class SingleErrorExceptionResolver implements DataFetcherExceptionResolver {
@Override
public final Mono<List<GraphQLError>> resolveException(Throwable exception, DataFetchingEnvironment environment) {
return doResolve(exception, environment).map(Collections::singletonList);
}
/**
* Implement this method to resolve the exception to an error.
*/
protected abstract Mono<GraphQLError> doResolve(Throwable exception, DataFetchingEnvironment environment);
}

View File

@@ -27,11 +27,13 @@ import graphql.GraphQL;
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.idl.RuntimeWiring;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
@@ -46,13 +48,19 @@ public class ExceptionResolversExceptionHandlerTests {
"Query", "greeting", env -> {
throw new IllegalArgumentException("Invalid greeting");
},
(ex, env) -> Collections.singletonList(
GraphqlErrorBuilder.newError(env)
.message("Resolved error: " + ex.getMessage())
.errorType(ErrorType.BAD_REQUEST)
.build()));
new SingleErrorExceptionResolver() {
@Override
protected Mono<GraphQLError> doResolve(Throwable ex, DataFetchingEnvironment env) {
return Mono.deferContextual(view ->
Mono.just(GraphqlErrorBuilder.newError(env)
.message("Resolved error: " + ex.getMessage() + ", name=" + view.get("name"))
.errorType(ErrorType.BAD_REQUEST)
.build()));
}});
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
ContextManager.setReactorContext(Context.of("name", "007"), input);
ExecutionResult result = graphQl.executeAsync(input).get();
Map<String, Object> data = result.getData();
@@ -61,7 +69,7 @@ public class ExceptionResolversExceptionHandlerTests {
List<GraphQLError> errors = result.getErrors();
assertThat(errors).hasSize(1);
GraphQLError error = errors.get(0);
assertThat(error.getMessage()).isEqualTo("Resolved error: Invalid greeting");
assertThat(error.getMessage()).isEqualTo("Resolved error: Invalid greeting, name=007");
assertThat(error.getErrorType().toString()).isEqualTo("BAD_REQUEST");
}
@@ -70,7 +78,8 @@ public class ExceptionResolversExceptionHandlerTests {
GraphQL graphQl = graphQl("type Query { greeting: String }",
"Query", "greeting", env -> {
throw new IllegalArgumentException("Invalid greeting");
});
},
(exception, environment) -> Mono.empty());
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
ExecutionResult result = graphQl.executeAsync(input).get();
@@ -85,9 +94,25 @@ public class ExceptionResolversExceptionHandlerTests {
assertThat(error.getErrorType().toString()).isEqualTo("INTERNAL_ERROR");
}
@Test
void suppressedException() throws Exception {
GraphQL graphQl = graphQl("type Query { greeting: String }",
"Query", "greeting", env -> {
throw new IllegalArgumentException("Invalid greeting");
},
(ex, env) -> Mono.just(Collections.emptyList()));
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
ExecutionResult result = graphQl.executeAsync(input).get();
Map<String, Object> data = result.getData();
assertThat(data).hasSize(1).containsEntry("greeting", null);
assertThat(result.getErrors()).hasSize(0);
}
private GraphQL graphQl(String schemaContent,
String typeName, String fieldName, DataFetcher<?> dataFetcher,
@Nullable DataFetcherExceptionResolver... resolvers) {
DataFetcherExceptionResolver... resolvers) {
RuntimeWiring wiring = RuntimeWiring.newRuntimeWiring()
.type(typeName, builder -> builder.dataFetcher(fieldName, dataFetcher))