Add DataFetcherExceptionResolver mechanism
- Support for one or more DataFetcherExceptionResolver beans - ErrorType enum with common error categories - Default handling of unresolved exceptions as ErrorType.INTERNAL_ERROR Closes gh-51
This commit is contained in:
@@ -28,6 +28,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.graphql.DataFetcherExceptionResolver;
|
||||
import org.springframework.graphql.support.GraphQLSource;
|
||||
|
||||
@Configuration
|
||||
@@ -56,11 +57,14 @@ public class GraphQLAutoConfiguration {
|
||||
@Bean
|
||||
public GraphQLSource.Builder graphQLSourceBuilder(
|
||||
GraphQLProperties properties, RuntimeWiring runtimeWiring,
|
||||
ObjectProvider<DataFetcherExceptionResolver> exceptionResolversProvider,
|
||||
ResourceLoader resourceLoader, ObjectProvider<Instrumentation> instrumentationsProvider) {
|
||||
|
||||
String schemaLocation = properties.getSchema().getLocation();
|
||||
return GraphQLSource.builder()
|
||||
.schemaResource(resourceLoader.getResource(schemaLocation))
|
||||
.runtimeWiring(runtimeWiring)
|
||||
.exceptionResolvers(exceptionResolversProvider.orderedStream().collect(Collectors.toList()))
|
||||
.instrumentation(instrumentationsProvider.orderedStream().collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import graphql.GraphQLError;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public interface DataFetcherExceptionResolver {
|
||||
|
||||
/**
|
||||
* Resolve the given exception and return errors 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.
|
||||
*/
|
||||
@Nullable
|
||||
List<GraphQLError> resolveException(Throwable exception, DataFetchingEnvironment environment);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import graphql.ErrorClassification;
|
||||
|
||||
/**
|
||||
* Common categories to use to classify for exceptions raised by
|
||||
* {@link graphql.schema.DataFetcher}'s that can enable a client to make
|
||||
* automated decisions.
|
||||
*
|
||||
* @see graphql.GraphqlErrorBuilder#errorType(ErrorClassification)
|
||||
*/
|
||||
public enum ErrorType implements ErrorClassification {
|
||||
|
||||
/**
|
||||
* {@link graphql.schema.DataFetcher} cannot or will not fetch the data value
|
||||
* due to something that is perceived to be a client error.
|
||||
*/
|
||||
BAD_REQUEST,
|
||||
|
||||
/**
|
||||
* {@link graphql.schema.DataFetcher} did not fetch the data value due to a
|
||||
* lack of valid authentication credentials.
|
||||
*/
|
||||
UNAUTHORIZED,
|
||||
|
||||
/**
|
||||
* {@link graphql.schema.DataFetcher} refuses to authorize the fetching of
|
||||
* the data value.
|
||||
*/
|
||||
FORBIDDEN,
|
||||
|
||||
/**
|
||||
* {@link graphql.schema.DataFetcher} did not find a data value or is not
|
||||
* willing to disclose that one exists.
|
||||
*/
|
||||
NOT_FOUND,
|
||||
|
||||
/**
|
||||
* {@link graphql.schema.DataFetcher} encountered an unexpected condition
|
||||
* that prevented it from fetching the data value.
|
||||
*/
|
||||
INTERNAL_ERROR;
|
||||
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import graphql.schema.idl.SchemaParser;
|
||||
import graphql.schema.idl.TypeDefinitionRegistry;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.graphql.DataFetcherExceptionResolver;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -48,6 +49,8 @@ class DefaultGraphQLSourceBuilder implements GraphQLSource.Builder {
|
||||
|
||||
private RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring().build();
|
||||
|
||||
private final List<DataFetcherExceptionResolver> exceptionResolvers = new ArrayList<>();
|
||||
|
||||
private final List<GraphQLTypeVisitor> typeVisitors = new ArrayList<>();
|
||||
|
||||
private final List<Instrumentation> instrumentations = new ArrayList<>();
|
||||
@@ -73,6 +76,12 @@ class DefaultGraphQLSourceBuilder implements GraphQLSource.Builder {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQLSource.Builder exceptionResolvers(List<DataFetcherExceptionResolver> resolvers) {
|
||||
this.exceptionResolvers.addAll(resolvers);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQLSource.Builder typeVisitors(List<GraphQLTypeVisitor> typeVisitors) {
|
||||
this.typeVisitors.addAll(typeVisitors);
|
||||
@@ -101,6 +110,7 @@ class DefaultGraphQLSourceBuilder implements GraphQLSource.Builder {
|
||||
}
|
||||
|
||||
GraphQL.Builder builder = GraphQL.newGraphQL(schema);
|
||||
builder.defaultDataFetcherExceptionHandler(new ExceptionResolversExceptionHandler(this.exceptionResolvers));
|
||||
if (!this.instrumentations.isEmpty()) {
|
||||
builder = builder.instrumentation(new ChainedInstrumentation(this.instrumentations));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import graphql.GraphQLError;
|
||||
import graphql.GraphqlErrorBuilder;
|
||||
import graphql.execution.DataFetcherExceptionHandler;
|
||||
import graphql.execution.DataFetcherExceptionHandlerParameters;
|
||||
import graphql.execution.DataFetcherExceptionHandlerResult;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.graphql.DataFetcherExceptionResolver;
|
||||
import org.springframework.graphql.ErrorType;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link DataFetcherExceptionHandler} that invokes {@link DataFetcherExceptionResolver}'s
|
||||
* in a sequence until one returns a non-null list of {@link GraphQLError}'s.
|
||||
*/
|
||||
class ExceptionResolversExceptionHandler implements DataFetcherExceptionHandler {
|
||||
|
||||
private final List<DataFetcherExceptionResolver> resolvers;
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance
|
||||
* @param resolvers the resolvers to use
|
||||
*/
|
||||
public ExceptionResolversExceptionHandler(List<DataFetcherExceptionResolver> resolvers) {
|
||||
Assert.notNull(resolvers, "'resolvers' is required");
|
||||
this.resolvers = new ArrayList<>(resolvers);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters parameters) {
|
||||
return invokeChain(parameters.getException(), parameters.getDataFetchingEnvironment());
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public GraphQLError applyDefaultHandling(Throwable ex, DataFetchingEnvironment env) {
|
||||
return GraphqlErrorBuilder.newError(env)
|
||||
.message(ex.getMessage())
|
||||
.errorType(ErrorType.INTERNAL_ERROR)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import graphql.schema.idl.RuntimeWiring;
|
||||
import graphql.schema.idl.TypeDefinitionRegistry;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.graphql.DataFetcherExceptionResolver;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
@@ -78,7 +79,14 @@ public interface GraphQLSource {
|
||||
Builder runtimeWiring(RuntimeWiring runtimeWiring);
|
||||
|
||||
/**
|
||||
* Add {@link GraphQLTypeVisitor}s to transform the underlying
|
||||
* Add {@link DataFetcherExceptionResolver}'s to use for resolving
|
||||
* exceptions from {@link graphql.schema.DataFetcher}'s.
|
||||
* @param resolvers the resolvers to add
|
||||
*/
|
||||
Builder exceptionResolvers(List<DataFetcherExceptionResolver> resolvers);
|
||||
|
||||
/**
|
||||
* Add {@link GraphQLTypeVisitor}'s to transform the underlying
|
||||
* {@link graphql.schema.GraphQLSchema} with.
|
||||
* @see graphql.schema.SchemaTransformer#transformSchema(GraphQLSchema, GraphQLTypeVisitor)
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import graphql.ExecutionInput;
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.GraphQL;
|
||||
import graphql.GraphQLError;
|
||||
import graphql.GraphqlErrorBuilder;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.graphql.DataFetcherExceptionResolver;
|
||||
import org.springframework.graphql.ErrorType;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ExceptionResolversExceptionHandler}.
|
||||
*/
|
||||
public class ExceptionResolversExceptionHandlerTests {
|
||||
|
||||
@Test
|
||||
void resolveException() throws Exception {
|
||||
GraphQL graphQL = graphQL("type Query { greeting: String }",
|
||||
"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()));
|
||||
|
||||
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);
|
||||
|
||||
List<GraphQLError> errors = result.getErrors();
|
||||
assertThat(errors).hasSize(1);
|
||||
GraphQLError error = errors.get(0);
|
||||
assertThat(error.getMessage()).isEqualTo("Resolved error: Invalid greeting");
|
||||
assertThat(error.getErrorType().toString()).isEqualTo("BAD_REQUEST");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unresolvedException() throws Exception {
|
||||
GraphQL graphQL = graphQL("type Query { greeting: String }",
|
||||
"Query", "greeting", env -> {
|
||||
throw new IllegalArgumentException("Invalid greeting");
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
List<GraphQLError> errors = result.getErrors();
|
||||
assertThat(errors).hasSize(1);
|
||||
GraphQLError error = errors.get(0);
|
||||
assertThat(error.getMessage()).isEqualTo("Invalid greeting");
|
||||
assertThat(error.getErrorType().toString()).isEqualTo("INTERNAL_ERROR");
|
||||
}
|
||||
|
||||
private GraphQL graphQL(String schemaContent,
|
||||
String typeName, String fieldName, DataFetcher<?> dataFetcher,
|
||||
@Nullable DataFetcherExceptionResolver... resolvers) {
|
||||
|
||||
RuntimeWiring wiring = RuntimeWiring.newRuntimeWiring()
|
||||
.type(typeName, builder -> builder.dataFetcher(fieldName, dataFetcher))
|
||||
.build();
|
||||
|
||||
return GraphQLSource.builder()
|
||||
.schemaResource(new ByteArrayResource(schemaContent.getBytes(StandardCharsets.UTF_8)))
|
||||
.runtimeWiring(wiring)
|
||||
.exceptionResolvers(Arrays.asList(resolvers))
|
||||
.build()
|
||||
.graphQL();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user