diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolverAdapter.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolverAdapter.java index 09b569e4..20110cff 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolverAdapter.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolverAdapter.java @@ -17,6 +17,7 @@ package org.springframework.graphql.execution; import java.util.Collections; import java.util.List; +import java.util.function.BiFunction; import graphql.GraphQLError; import graphql.schema.DataFetchingEnvironment; @@ -27,23 +28,34 @@ import org.springframework.lang.Nullable; /** * Adapter for {@link DataFetcherExceptionResolver} that pre-implements the - * asynchronous contract and exposes the following synchronous methods: + * asynchronous contract and exposes the following synchronous protected methods: * * + *

Use {@link #from(BiFunction)} to create an instance or extend this class + * and override one of its resolve methods. + * *

Implementations can also express interest in ThreadLocal context * propagation, from the underlying transport thread, via * {@link #setThreadLocalContextAware(boolean)}. * * @author Rossen Stoyanchev */ -public class DataFetcherExceptionResolverAdapter implements DataFetcherExceptionResolver { +public abstract class DataFetcherExceptionResolverAdapter implements DataFetcherExceptionResolver { private boolean threadLocalContextAware; + /** + * Protected constructor since this class is meant to be extended to provide + * the actual exception resolution logic. + */ + protected DataFetcherExceptionResolverAdapter() { + } + + /** * Sub-classes can set this to indicate that ThreadLocal context from the * transport handler (e.g. HTTP handler) should be restored when resolving @@ -111,4 +123,23 @@ public class DataFetcherExceptionResolverAdapter implements DataFetcherException return null; } + + /** + * Factory method to create a {@link DataFetcherExceptionResolverAdapter} that + * resolves exceptions with the given {@code BiFunction}. + * @param resolver the resolver function to use + * @return the created instance + */ + public static DataFetcherExceptionResolverAdapter from( + BiFunction resolver) { + + return new DataFetcherExceptionResolverAdapter() { + + @Override + protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) { + return resolver.apply(ex, env); + } + }; + } + } 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 a09b1585..3cb0efc4 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 @@ -18,11 +18,10 @@ package org.springframework.graphql.execution; import java.time.Duration; import java.util.Collections; -import java.util.function.BiFunction; +import java.util.List; import graphql.ExecutionInput; import graphql.ExecutionResult; -import graphql.GraphQL; import graphql.GraphQLError; import graphql.GraphqlErrorBuilder; import graphql.schema.DataFetchingEnvironment; @@ -43,16 +42,25 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class ExceptionResolversExceptionHandlerTests { + private final GraphQlSetup graphQlSetup = + GraphQlSetup.schemaContent("type Query { greeting: String }") + .queryFetcher("greeting", (env) -> { + throw new IllegalArgumentException("Invalid greeting"); + }); + + private final ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build(); + + @Test void resolveException() throws Exception { - GraphQL graphQl = graphQl((ex, env) -> - Mono.just(Collections.singletonList( + DataFetcherExceptionResolver resolver = + DataFetcherExceptionResolverAdapter.from((ex, env) -> GraphqlErrorBuilder.newError(env) .message("Resolved error: " + ex.getMessage()) - .errorType(ErrorType.BAD_REQUEST).build()))); + .errorType(ErrorType.BAD_REQUEST).build()); - ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build(); - ExecutionResult result = graphQl.executeAsync(input).get(); + ExecutionResult result = this.graphQlSetup.exceptionResolver(resolver).toGraphQl() + .executeAsync(this.input).get(); GraphQlResponse response = GraphQlResponse.from(result); assertThat(response.errorCount()).isEqualTo(1); @@ -65,16 +73,16 @@ public class ExceptionResolversExceptionHandlerTests { @Test void resolveExceptionWithReactorContext() throws Exception { - GraphQL graphQl = graphQl((ex, env) -> - Mono.deferContextual((view) -> Mono.just(Collections.singletonList( + DataFetcherExceptionResolver resolver = + (ex, env) -> Mono.deferContextual((view) -> Mono.just(Collections.singletonList( GraphqlErrorBuilder.newError(env) .message("Resolved error: " + ex.getMessage() + ", name=" + view.get("name")) - .errorType(ErrorType.BAD_REQUEST).build())))); + .errorType(ErrorType.BAD_REQUEST).build()))); - ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build(); ReactorContextManager.setReactorContext(Context.of("name", "007"), input); - ExecutionResult result = graphQl.executeAsync(input).get(); + ExecutionResult result = this.graphQlSetup.exceptionResolver(resolver).toGraphQl() + .executeAsync(this.input).get(); GraphQlResponse response = GraphQlResponse.from(result); assertThat(response.errorCount()).isEqualTo(1); @@ -87,18 +95,20 @@ public class ExceptionResolversExceptionHandlerTests { nameThreadLocal.set("007"); TestThreadLocalAccessor accessor = new TestThreadLocalAccessor<>(nameThreadLocal); try { - GraphQL graphQl = graphQl(threadLocalContextAwareResolver((ex, env) -> - GraphqlErrorBuilder.newError(env) - .message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get()) - .errorType(ErrorType.BAD_REQUEST) - .build())); + DataFetcherExceptionResolverAdapter resolver = + DataFetcherExceptionResolverAdapter.from((ex, env) -> + GraphqlErrorBuilder.newError(env) + .message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get()) + .errorType(ErrorType.BAD_REQUEST) + .build()); + + resolver.setThreadLocalContextAware(true); - ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build(); ContextView view = ReactorContextManager.extractThreadLocalValues(accessor, Context.empty()); ReactorContextManager.setReactorContext(view, input); - Mono result = Mono.delay(Duration.ofMillis(10)) - .flatMap((aLong) -> Mono.fromFuture(graphQl.executeAsync(input))); + Mono result = Mono.delay(Duration.ofMillis(10)).flatMap((aLong) -> + Mono.fromFuture(this.graphQlSetup.exceptionResolver(resolver).toGraphQl().executeAsync(this.input))); GraphQlResponse response = GraphQlResponse.from(result); assertThat(response.errorCount()).isEqualTo(1); @@ -111,10 +121,11 @@ public class ExceptionResolversExceptionHandlerTests { @Test void unresolvedException() throws Exception { - GraphQL graphQl = graphQl((exception, environment) -> Mono.empty()); + DataFetcherExceptionResolverAdapter resolver = + DataFetcherExceptionResolverAdapter.from((ex, env) -> null); - ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build(); - ExecutionResult result = graphQl.executeAsync(input).get(); + ExecutionResult result = this.graphQlSetup.exceptionResolver(resolver).toGraphQl() + .executeAsync(this.input).get(); GraphQlResponse response = GraphQlResponse.from(result); assertThat(response.errorCount()).isEqualTo(1); @@ -127,36 +138,13 @@ public class ExceptionResolversExceptionHandlerTests { @Test void suppressedException() throws Exception { - GraphQL graphQl = graphQl((ex, env) -> Mono.just(Collections.emptyList())); - ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build(); - ExecutionResult result = graphQl.executeAsync(input).get(); + ExecutionResult result = this.graphQlSetup + .exceptionResolver((ex, env) -> Mono.just(Collections.emptyList())).toGraphQl() + .executeAsync(input).get(); String greeting = GraphQlResponse.from(result).rawValue("greeting"); assertThat(greeting).isNull(); } - private static GraphQL graphQl(DataFetcherExceptionResolver exceptionResolver) { - return GraphQlSetup.schemaContent("type Query { greeting: String }") - .queryFetcher("greeting", (env) -> { - throw new IllegalArgumentException("Invalid greeting"); - }) - .exceptionResolver(exceptionResolver) - .toGraphQl(); - } - - private static DataFetcherExceptionResolver threadLocalContextAwareResolver( - BiFunction resolver) { - - DataFetcherExceptionResolverAdapter adapter = new DataFetcherExceptionResolverAdapter() { - - @Override - protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) { - return resolver.apply(ex, env); - } - }; - adapter.setThreadLocalContextAware(true); - return adapter; - } - } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/WebGraphQlHandlerTests.java b/spring-graphql/src/test/java/org/springframework/graphql/web/WebGraphQlHandlerTests.java index cc9c848b..5d5ee9d1 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/WebGraphQlHandlerTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/WebGraphQlHandlerTests.java @@ -19,11 +19,9 @@ package org.springframework.graphql.web; import java.net.URI; import java.time.Duration; import java.util.Collections; -import java.util.function.BiFunction; -import graphql.GraphQLError; import graphql.GraphqlErrorBuilder; -import graphql.schema.DataFetchingEnvironment; +import graphql.schema.DataFetcher; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -45,18 +43,25 @@ public class WebGraphQlHandlerTests { private static final WebInput webInput = new WebInput( URI.create("https://abc.org"), new HttpHeaders(), Collections.singletonMap("query", "{ greeting }"), null, "1"); + + private final GraphQlSetup graphQlSetup = GraphQlSetup.schemaContent("type Query { greeting: String }"); + + private final DataFetcher errorDataFetcher = env -> { + throw new IllegalArgumentException("Invalid greeting"); + }; + + @Test void reactorContextPropagation() { - WebGraphQlHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }") - .queryFetcher("greeting", (env) -> - Mono.deferContextual((context) -> { - Object name = context.get("name"); - return Mono.delay(Duration.ofMillis(50)).map((aLong) -> "Hello " + name); - })) - .toWebGraphQlHandler(); + DataFetcher dataFetcher = (env) -> Mono.deferContextual((context) -> { + Object name = context.get("name"); + return Mono.delay(Duration.ofMillis(50)).map((aLong) -> "Hello " + name); + }); - Mono outputMono = handler.handleRequest(webInput) - .contextWrite((context) -> context.put("name", "007")); + Mono outputMono = + this.graphQlSetup.queryFetcher("greeting", dataFetcher).toWebGraphQlHandler() + .handleRequest(webInput) + .contextWrite((context) -> context.put("name", "007")); String greeting = GraphQlResponse.from(outputMono).toEntity("greeting", String.class); assertThat(greeting).isEqualTo("Hello 007"); @@ -64,19 +69,18 @@ public class WebGraphQlHandlerTests { @Test void reactorContextPropagationToExceptionResolver() { - WebGraphQlHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }") - .queryFetcher("greeting", (env) -> { - throw new IllegalArgumentException("Invalid greeting"); - }) - .exceptionResolver((ex, env) -> Mono.deferContextual((view) -> - Mono.just(Collections.singletonList( - GraphqlErrorBuilder.newError(env) - .message("Resolved error: " + ex.getMessage() + ", name=" + view.get("name")) - .errorType(ErrorType.BAD_REQUEST) - .build())))) - .toWebGraphQlHandler(); + DataFetcherExceptionResolver exceptionResolver = + (ex, env) -> Mono.deferContextual((view) -> Mono.just(Collections.singletonList( + GraphqlErrorBuilder.newError(env) + .message("Resolved error: " + ex.getMessage() + ", name=" + view.get("name")) + .errorType(ErrorType.BAD_REQUEST) + .build()))); - Mono outputMono = handler.handleRequest(webInput).contextWrite((cxt) -> cxt.put("name", "007")); + Mono outputMono = this.graphQlSetup.queryFetcher("greeting", this.errorDataFetcher) + .exceptionResolver(exceptionResolver) + .toWebGraphQlHandler() + .handleRequest(webInput) + .contextWrite((cxt) -> cxt.put("name", "007")); GraphQlResponse response = GraphQlResponse.from(outputMono); assertThat(response.errorCount()).isEqualTo(1); @@ -92,13 +96,13 @@ public class WebGraphQlHandlerTests { nameThreadLocal.set("007"); TestThreadLocalAccessor threadLocalAccessor = new TestThreadLocalAccessor<>(nameThreadLocal); try { - WebGraphQlHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }") + + Mono outputMono = this.graphQlSetup .queryFetcher("greeting", env -> "Hello " + nameThreadLocal.get()) .webInterceptor((input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap((aLong) -> next.next(input))) .threadLocalAccessor(threadLocalAccessor) - .toWebGraphQlHandler(); - - Mono outputMono = handler.handleRequest(webInput); + .toWebGraphQlHandler() + .handleRequest(webInput); String greeting = GraphQlResponse.from(outputMono).toEntity("greeting", String.class); assertThat(greeting).isEqualTo("Hello 007"); @@ -114,20 +118,18 @@ public class WebGraphQlHandlerTests { nameThreadLocal.set("007"); TestThreadLocalAccessor threadLocalAccessor = new TestThreadLocalAccessor<>(nameThreadLocal); try { - WebGraphQlHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }") - .queryFetcher("greeting", env -> { - throw new IllegalArgumentException("Invalid greeting"); - }) - .exceptionResolver(threadLocalContextAwareExceptionResolver((ex, env) -> - GraphqlErrorBuilder.newError(env) - .message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get()) - .errorType(ErrorType.BAD_REQUEST).build()) - ) + DataFetcherExceptionResolverAdapter exceptionResolver = DataFetcherExceptionResolverAdapter.from((ex, env) -> + GraphqlErrorBuilder.newError(env) + .message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get()) + .errorType(ErrorType.BAD_REQUEST).build()); + exceptionResolver.setThreadLocalContextAware(true); + + Mono outputMono = this.graphQlSetup.queryFetcher("greeting", this.errorDataFetcher) + .exceptionResolver(exceptionResolver) .webInterceptor((input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap((aLong) -> next.next(input))) .threadLocalAccessor(threadLocalAccessor) - .toWebGraphQlHandler(); - - Mono outputMono = handler.handleRequest(webInput); + .toWebGraphQlHandler() + .handleRequest(webInput); GraphQlResponse response = GraphQlResponse.from(outputMono); assertThat(response.errorCount()).isEqualTo(1); @@ -138,18 +140,4 @@ public class WebGraphQlHandlerTests { } } - private static DataFetcherExceptionResolver threadLocalContextAwareExceptionResolver( - BiFunction resolver) { - - DataFetcherExceptionResolverAdapter adapter = new DataFetcherExceptionResolverAdapter() { - - @Override - protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) { - return resolver.apply(ex, env); - } - }; - adapter.setThreadLocalContextAware(true); - return adapter; - } - }