Update DataFetcherExceptionResolverAdapter

Make it protected since it needs to be extended and add factory method
with a BiFunction as an alternative.
This commit is contained in:
Rossen Stoyanchev
2021-11-17 11:27:20 +00:00
parent e2a53759cb
commit b69b92d2c8
3 changed files with 112 additions and 105 deletions

View File

@@ -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:
* <ul>
* <li>{@link #resolveToSingleError}
* <li>{@link #resolveToMultipleErrors}
* </ul>
*
* <p>Use {@link #from(BiFunction)} to create an instance or extend this class
* and override one of its resolve methods.
*
* <p>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<Throwable, DataFetchingEnvironment, GraphQLError> resolver) {
return new DataFetcherExceptionResolverAdapter() {
@Override
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
return resolver.apply(ex, env);
}
};
}
}

View File

@@ -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<String> 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<ExecutionResult> result = Mono.delay(Duration.ofMillis(10))
.flatMap((aLong) -> Mono.fromFuture(graphQl.executeAsync(input)));
Mono<ExecutionResult> 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<Throwable, DataFetchingEnvironment, GraphQLError> resolver) {
DataFetcherExceptionResolverAdapter adapter = new DataFetcherExceptionResolverAdapter() {
@Override
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
return resolver.apply(ex, env);
}
};
adapter.setThreadLocalContextAware(true);
return adapter;
}
}

View File

@@ -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<Object> 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<Object> dataFetcher = (env) -> Mono.deferContextual((context) -> {
Object name = context.get("name");
return Mono.delay(Duration.ofMillis(50)).map((aLong) -> "Hello " + name);
});
Mono<WebOutput> outputMono = handler.handleRequest(webInput)
.contextWrite((context) -> context.put("name", "007"));
Mono<WebOutput> 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<WebOutput> outputMono = handler.handleRequest(webInput).contextWrite((cxt) -> cxt.put("name", "007"));
Mono<WebOutput> 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<String> threadLocalAccessor = new TestThreadLocalAccessor<>(nameThreadLocal);
try {
WebGraphQlHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }")
Mono<WebOutput> 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<WebOutput> 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<String> 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<WebOutput> 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<WebOutput> 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<Throwable, DataFetchingEnvironment, GraphQLError> resolver) {
DataFetcherExceptionResolverAdapter adapter = new DataFetcherExceptionResolverAdapter() {
@Override
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
return resolver.apply(ex, env);
}
};
adapter.setThreadLocalContextAware(true);
return adapter;
}
}