Reactive return type for DataFetcherExceptionResolver
Closes gh-52
This commit is contained in:
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user