diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolver.java
index c053032c..19d7966c 100644
--- a/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolver.java
+++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolver.java
@@ -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.
+ *
+ *
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.
*
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 resolveException(Throwable exception, DataFetchingEnvironment environment);
+ Mono> resolveException(Throwable exception, DataFetchingEnvironment environment);
}
diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandler.java
index c2ef22a3..b113eafa 100644
--- a/spring-graphql/src/main/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandler.java
+++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandler.java
@@ -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 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)
diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/SingleErrorExceptionResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/SingleErrorExceptionResolver.java
new file mode 100644
index 00000000..0683ef31
--- /dev/null
+++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/SingleErrorExceptionResolver.java
@@ -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> 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 doResolve(Throwable exception, DataFetchingEnvironment environment);
+
+}
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandlerTests.java b/spring-graphql/src/test/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandlerTests.java
index 535d8e5f..ae46c117 100644
--- a/spring-graphql/src/test/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandlerTests.java
+++ b/spring-graphql/src/test/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandlerTests.java
@@ -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 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 data = result.getData();
@@ -61,7 +69,7 @@ public class ExceptionResolversExceptionHandlerTests {
List 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 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))