Skip to content

Commit bac7461

Browse files
committed
Add DataFetcherExceptionResolverAdapter
The adapter aims to: - simplify the common case of synchronous resolution to a single error - support ThreadLocal context propagation on an opt-in basis This replaces the SyncDataFetcherExceptionResolver and removes the need to propagate ThreadLocal context to every resolver.
1 parent a6211bd commit bac7461

File tree

8 files changed

+186
-92
lines changed

8 files changed

+186
-92
lines changed
Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
package io.spring.sample.graphql;
22

3-
import java.util.Collections;
4-
import java.util.List;
5-
63
import graphql.GraphQLError;
74
import graphql.GraphqlErrorBuilder;
85
import graphql.schema.DataFetchingEnvironment;
96

7+
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
108
import org.springframework.graphql.execution.ErrorType;
11-
import org.springframework.graphql.execution.SyncDataFetcherExceptionResolver;
129
import org.springframework.security.access.AccessDeniedException;
1310
import org.springframework.security.authentication.AuthenticationTrustResolver;
1411
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
@@ -20,22 +17,22 @@
2017
import org.springframework.util.Assert;
2118

2219
@Component
23-
public class SecurityDataFetcherExceptionResolver implements SyncDataFetcherExceptionResolver {
20+
public class SecurityDataFetcherExceptionResolver extends DataFetcherExceptionResolverAdapter {
2421

2522
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
2623

2724
@Override
28-
public List<GraphQLError> doResolveException(Throwable exception, DataFetchingEnvironment environment) {
29-
if (exception instanceof AuthenticationException) {
30-
return unauthorized(environment);
25+
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
26+
if (ex instanceof AuthenticationException) {
27+
return unauthorized(env);
3128
}
32-
if (exception instanceof AccessDeniedException) {
29+
if (ex instanceof AccessDeniedException) {
3330
SecurityContext context = SecurityContextHolder.getContext();
3431
Authentication authentication = context.getAuthentication();
3532
if (this.authenticationTrustResolver.isAnonymous(authentication)) {
36-
return unauthorized(environment);
33+
return unauthorized(env);
3734
}
38-
return forbidden(environment);
35+
return forbidden(env);
3936
}
4037
return null;
4138
}
@@ -45,20 +42,18 @@ public void setAuthenticationTrustResolver(AuthenticationTrustResolver authentic
4542
this.authenticationTrustResolver = authenticationTrustResolver;
4643
}
4744

48-
private List<GraphQLError> unauthorized(DataFetchingEnvironment environment) {
49-
return Collections.singletonList(
50-
GraphqlErrorBuilder.newError(environment)
51-
.errorType(ErrorType.UNAUTHORIZED)
52-
.message("Unauthorized")
53-
.build());
45+
private GraphQLError unauthorized(DataFetchingEnvironment environment) {
46+
return GraphqlErrorBuilder.newError(environment)
47+
.errorType(ErrorType.UNAUTHORIZED)
48+
.message("Unauthorized")
49+
.build();
5450
}
5551

56-
private List<GraphQLError> forbidden(DataFetchingEnvironment environment) {
57-
return Collections.singletonList(
58-
GraphqlErrorBuilder.newError(environment)
52+
private GraphQLError forbidden(DataFetchingEnvironment environment) {
53+
return GraphqlErrorBuilder.newError(environment)
5954
.errorType(ErrorType.FORBIDDEN)
6055
.message("Forbidden")
61-
.build());
56+
.build();
6257
}
6358

6459
}

spring-graphql-docs/src/docs/asciidoc/index.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,11 @@ by the <<execution-graphqlsource>> builder. It enables applications to register
271271
more Spring `DataFetcherExceptionResolver` components that are invoked sequentially
272272
until one resolves the `Exception` to a list of `graphql.GraphQLError` objects.
273273

274+
`DataFetcherExceptionResolver` is an asynchronous contract. For most implementations, it
275+
would be sufficient to extend `DataFetcherExceptionResolverAdapter` and override
276+
one of its `resolveToSingleError` or `resolveToMultipleErrors` methods that
277+
resolve exceptions synchronously.
278+
274279
A `GraphQLError` can be assigned an `graphql.ErrorClassification`. Spring GraphQL
275280
defines an `ErrorType` enum with common, error classification categories:
276281

spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolver.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,33 @@
2323
import reactor.core.publisher.Mono;
2424

2525
/**
26-
* Contract for resolving exceptions from {@link graphql.schema.DataFetcher}'s
27-
* to {@code GraphQLError}'s to be added to the GraphQL response, possibly also
28-
* using Spring's {@link graphql.ErrorType} for the error category.
26+
* Contract to resolve exceptions from {@link graphql.schema.DataFetcher}s.
27+
* Implementations are typically declared as beans in Spring configuration and
28+
* are invoked sequentially until one emits a List of {@link GraphQLError}s.
2929
*
30-
* <p>Implementations are typically declared as beans in Spring configuration
31-
* and invoked in order until one emits a List.
30+
* <p>Most resolver implementations can extend
31+
* {@link DataFetcherExceptionResolverAdapter} and override one of its
32+
* {@link DataFetcherExceptionResolverAdapter#resolveToSingleError resolveToSingleError} or
33+
* {@link DataFetcherExceptionResolverAdapter#resolveToMultipleErrors resolveToMultipleErrors}
34+
* methods that resolve the exception synchronously.
35+
*
36+
* <p>Resolver implementations can use {@link ErrorType} to classify errors
37+
* using one of several common categories.
3238
*
3339
* @author Rossen Stoyanchev
3440
* @since 1.0.0
35-
* @see SyncDataFetcherExceptionResolver
41+
* @see ErrorType
42+
* @see DataFetcherExceptionResolverAdapter
43+
* @see ExceptionResolversExceptionHandler
3644
*/
3745
public interface DataFetcherExceptionResolver {
3846

3947
/**
4048
* Resolve the given exception and return the error(s) to add to the response.
41-
*
4249
* <p>Implementations can use
4350
* {@link graphql.GraphqlErrorBuilder#newError(DataFetchingEnvironment)} to
4451
* create an error with the coordinates of the target field, and use
4552
* {@link ErrorType} to specify a category for the error.
46-
*
4753
* @param exception the exception to resolve
4854
* @param environment the environment for the invoked {@code DataFetcher}
4955
* @return a {@code Mono} with errors to add to the GraphQL response;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.graphql.execution;
17+
18+
import java.util.Collections;
19+
import java.util.List;
20+
21+
import graphql.GraphQLError;
22+
import graphql.schema.DataFetchingEnvironment;
23+
import reactor.core.publisher.Mono;
24+
import reactor.util.context.ContextView;
25+
26+
import org.springframework.lang.Nullable;
27+
28+
/**
29+
* Adapter for {@link DataFetcherExceptionResolver} that pre-implements the
30+
* asynchronous contract and exposes the following synchronous methods:
31+
* <ul>
32+
* <li>{@link #resolveToSingleError}
33+
* <li>{@link #resolveToMultipleErrors}
34+
* </ul>
35+
*
36+
* <p>Implementations can also express interest in ThreadLocal context
37+
* propagation, from the underlying transport thread, via
38+
* {@link #setThreadLocalContextAware(boolean)}.
39+
*
40+
* @author Rossen Stoyanchev
41+
*/
42+
public class DataFetcherExceptionResolverAdapter implements DataFetcherExceptionResolver {
43+
44+
private boolean threadLocalContextAware;
45+
46+
47+
/**
48+
* Sub-classes can set this to indicate that ThreadLocal context from the
49+
* transport handler (e.g. HTTP handler) should be restored when resolving
50+
* exceptions.
51+
* <p><strong>Note:</strong> This property is applicable only if transports
52+
* use ThreadLocal's' (e.g. Spring MVC) and if a {@link ThreadLocalAccessor}
53+
* is registered to extract ThreadLocal values of interest. There is no
54+
* impact from setting this property otherwise.
55+
* <p>By default this is set to "false" in which case there is no attempt
56+
* to propagate ThreadLocal context.
57+
* @param threadLocalContextAware whether this resolver needs access to
58+
* ThreadLocal context or not.
59+
*/
60+
public void setThreadLocalContextAware(boolean threadLocalContextAware) {
61+
this.threadLocalContextAware = threadLocalContextAware;
62+
}
63+
64+
/**
65+
* Whether ThreadLocal context needs to be restored for this resolver.
66+
*/
67+
public boolean isThreadLocalContextAware() {
68+
return this.threadLocalContextAware;
69+
}
70+
71+
@Override
72+
public final Mono<List<GraphQLError>> resolveException(Throwable ex, DataFetchingEnvironment env) {
73+
return Mono.defer(() -> Mono.justOrEmpty(resolveInternal(ex, env)));
74+
}
75+
76+
@Nullable
77+
private List<GraphQLError> resolveInternal(Throwable ex, DataFetchingEnvironment env) {
78+
if (!this.threadLocalContextAware) {
79+
return resolveToMultipleErrors(ex, env);
80+
}
81+
ContextView contextView = ReactorContextManager.getReactorContext(env);
82+
try {
83+
ReactorContextManager.restoreThreadLocalValues(contextView);
84+
return resolveToMultipleErrors(ex, env);
85+
}
86+
finally {
87+
ReactorContextManager.resetThreadLocalValues(contextView);
88+
}
89+
}
90+
91+
/**
92+
* Override this method to resolve an Exception to multiple GraphQL errors.
93+
* @param ex the exception to resolve
94+
* @param env the environment for the invoked {@code DataFetcher}
95+
* @return the resolved errors or {@code null} if unresolved
96+
*/
97+
@Nullable
98+
protected List<GraphQLError> resolveToMultipleErrors(Throwable ex, DataFetchingEnvironment env) {
99+
GraphQLError error = resolveToSingleError(ex, env);
100+
return (error != null ? Collections.singletonList(error) : null);
101+
}
102+
103+
/**
104+
* Override this method to resolve an Exception to a single GraphQL error.
105+
* @param ex the exception to resolve
106+
* @param env the environment for the invoked {@code DataFetcher}
107+
* @return the resolved error or {@code null} if unresolved
108+
*/
109+
@Nullable
110+
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
111+
return null;
112+
}
113+
114+
}

spring-graphql/src/main/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandler.java

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ DataFetcherExceptionHandlerResult invokeChain(Throwable ex, DataFetchingEnvironm
7070
// https://github.com/graphql-java/graphql-java/issues/2356
7171
try {
7272
return Flux.fromIterable(this.resolvers)
73-
.flatMap((resolver) -> resolveErrors(ex, env, resolver))
73+
.flatMap((resolver) -> resolver.resolveException(ex, env))
7474
.next()
7575
.map((errors) -> DataFetcherExceptionHandlerResult.newResult().errors(errors).build())
7676
.switchIfEmpty(Mono.fromCallable(() -> applyDefaultHandling(ex, env)))
@@ -89,19 +89,6 @@ DataFetcherExceptionHandlerResult invokeChain(Throwable ex, DataFetchingEnvironm
8989
}
9090
}
9191

92-
private Mono<List<GraphQLError>> resolveErrors(
93-
Throwable ex, DataFetchingEnvironment environment, DataFetcherExceptionResolver resolver) {
94-
95-
ContextView contextView = ReactorContextManager.getReactorContext(environment);
96-
try {
97-
ReactorContextManager.restoreThreadLocalValues(contextView);
98-
return resolver.resolveException(ex, environment);
99-
}
100-
finally {
101-
ReactorContextManager.resetThreadLocalValues(contextView);
102-
}
103-
}
104-
10592
private DataFetcherExceptionHandlerResult applyDefaultHandling(Throwable ex, DataFetchingEnvironment env) {
10693
GraphQLError error = GraphqlErrorBuilder.newError(env)
10794
.message(ex.getMessage())

spring-graphql/src/main/java/org/springframework/graphql/execution/SyncDataFetcherExceptionResolver.java

Lines changed: 0 additions & 46 deletions
This file was deleted.

spring-graphql/src/test/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandlerTests.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
import java.util.Collections;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.function.BiFunction;
2324

2425
import graphql.ExecutionInput;
2526
import graphql.ExecutionResult;
2627
import graphql.GraphQL;
2728
import graphql.GraphQLError;
2829
import graphql.GraphqlErrorBuilder;
30+
import graphql.schema.DataFetchingEnvironment;
2931
import org.junit.jupiter.api.Test;
3032
import reactor.core.publisher.Mono;
3133
import reactor.util.context.Context;
@@ -96,7 +98,7 @@ void resolveExceptionWithThreadLocal() {
9698
(env) -> {
9799
throw new IllegalArgumentException("Invalid greeting");
98100
},
99-
(SyncDataFetcherExceptionResolver) (ex, env) -> Collections.singletonList(
101+
threadLocalContextAwareExceptionResolver((ex, env) ->
100102
GraphqlErrorBuilder.newError(env)
101103
.message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get())
102104
.errorType(ErrorType.BAD_REQUEST)
@@ -155,4 +157,18 @@ void suppressedException() throws Exception {
155157
assertThat(result.getErrors()).hasSize(0);
156158
}
157159

160+
private static DataFetcherExceptionResolver threadLocalContextAwareExceptionResolver(
161+
BiFunction<Throwable, DataFetchingEnvironment, GraphQLError> resolver) {
162+
163+
DataFetcherExceptionResolverAdapter adapter = new DataFetcherExceptionResolverAdapter() {
164+
165+
@Override
166+
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
167+
return resolver.apply(ex, env);
168+
}
169+
};
170+
adapter.setThreadLocalContextAware(true);
171+
return adapter;
172+
}
173+
158174
}

0 commit comments

Comments
 (0)