Skip to content

Commit a7e68d3

Browse files
Munoonrstoyanchev
authored andcommitted
Add SubscriptionExceptionResolver
See gh-398
1 parent 96f158b commit a7e68d3

16 files changed

+581
-94
lines changed

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

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,6 @@
1616

1717
package org.springframework.graphql.execution;
1818

19-
import java.util.ArrayList;
20-
import java.util.Collections;
21-
import java.util.List;
22-
import java.util.Map;
23-
import java.util.function.Consumer;
24-
2519
import graphql.GraphQL;
2620
import graphql.execution.instrumentation.ChainedInstrumentation;
2721
import graphql.execution.instrumentation.Instrumentation;
@@ -30,6 +24,9 @@
3024
import graphql.schema.GraphQLTypeVisitor;
3125
import graphql.schema.SchemaTraverser;
3226

27+
import java.util.*;
28+
import java.util.function.Consumer;
29+
3330

3431
/**
3532
* Implementation of {@link GraphQlSource.Builder} that leaves it to subclasses
@@ -43,6 +40,8 @@ abstract class AbstractGraphQlSourceBuilder<B extends GraphQlSource.Builder<B>>
4340

4441
private final List<DataFetcherExceptionResolver> exceptionResolvers = new ArrayList<>();
4542

43+
private final List<SubscriptionExceptionResolver> subscriptionExceptionResolvers = new ArrayList<>();
44+
4645
private final List<GraphQLTypeVisitor> typeVisitors = new ArrayList<>();
4746

4847
private final List<Instrumentation> instrumentations = new ArrayList<>();
@@ -57,6 +56,12 @@ public B exceptionResolvers(List<DataFetcherExceptionResolver> resolvers) {
5756
return self();
5857
}
5958

59+
@Override
60+
public B subscriptionExceptionResolvers(List<SubscriptionExceptionResolver> subscriptionExceptionResolvers) {
61+
this.subscriptionExceptionResolvers.addAll(subscriptionExceptionResolvers);
62+
return self();
63+
}
64+
6065
@Override
6166
public B typeVisitors(List<GraphQLTypeVisitor> typeVisitors) {
6267
this.typeVisitors.addAll(typeVisitors);
@@ -105,8 +110,12 @@ public GraphQlSource build() {
105110
protected abstract GraphQLSchema initGraphQlSchema();
106111

107112
private GraphQLSchema applyTypeVisitors(GraphQLSchema schema) {
113+
SubscriptionExceptionResolver subscriptionExceptionResolver = new DelegatingSubscriptionExceptionResolver(
114+
subscriptionExceptionResolvers);
115+
GraphQLTypeVisitor visitor = ContextDataFetcherDecorator.createVisitor(subscriptionExceptionResolver);
116+
108117
List<GraphQLTypeVisitor> visitors = new ArrayList<>(this.typeVisitors);
109-
visitors.add(ContextDataFetcherDecorator.TYPE_VISITOR);
118+
visitors.add(visitor);
110119

111120
GraphQLCodeRegistry.Builder codeRegistry = GraphQLCodeRegistry.newCodeRegistry(schema.getCodeRegistry());
112121
Map<Class<?>, Object> vars = Collections.singletonMap(GraphQLCodeRegistry.Builder.class, codeRegistry);

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

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,16 @@
1717
package org.springframework.graphql.execution;
1818

1919
import graphql.ExecutionInput;
20-
import graphql.schema.DataFetcher;
21-
import graphql.schema.DataFetchingEnvironment;
22-
import graphql.schema.GraphQLCodeRegistry;
23-
import graphql.schema.GraphQLFieldDefinition;
24-
import graphql.schema.GraphQLFieldsContainer;
25-
import graphql.schema.GraphQLSchemaElement;
26-
import graphql.schema.GraphQLTypeVisitor;
27-
import graphql.schema.GraphQLTypeVisitorStub;
20+
import graphql.schema.*;
2821
import graphql.util.TraversalControl;
2922
import graphql.util.TraverserContext;
3023
import org.reactivestreams.Publisher;
24+
import org.springframework.util.Assert;
3125
import reactor.core.publisher.Flux;
3226
import reactor.core.publisher.Mono;
3327
import reactor.util.context.ContextView;
3428

35-
import org.springframework.util.Assert;
29+
import java.util.function.Function;
3630

3731
/**
3832
* Wrap a {@link DataFetcher} to enable the following:
@@ -51,10 +45,16 @@ final class ContextDataFetcherDecorator implements DataFetcher<Object> {
5145

5246
private final boolean subscription;
5347

54-
private ContextDataFetcherDecorator(DataFetcher<?> delegate, boolean subscription) {
48+
private final SubscriptionExceptionResolver subscriptionExceptionResolver;
49+
50+
private ContextDataFetcherDecorator(
51+
DataFetcher<?> delegate, boolean subscription,
52+
SubscriptionExceptionResolver subscriptionExceptionResolver) {
5553
Assert.notNull(delegate, "'delegate' DataFetcher is required");
54+
Assert.notNull(subscriptionExceptionResolver, "'subscriptionExceptionResolver' is required");
5655
this.delegate = delegate;
5756
this.subscription = subscription;
57+
this.subscriptionExceptionResolver = subscriptionExceptionResolver;
5858
}
5959

6060
@Override
@@ -66,7 +66,8 @@ public Object get(DataFetchingEnvironment environment) throws Exception {
6666
ContextView contextView = ReactorContextManager.getReactorContext(environment.getGraphQlContext());
6767

6868
if (this.subscription) {
69-
return (!contextView.isEmpty() ? Flux.from((Publisher<?>) value).contextWrite(contextView) : value);
69+
Publisher<?> publisher = interceptSubscriptionPublisherWithExceptionHandler((Publisher<?>) value);
70+
return (!contextView.isEmpty() ? Flux.from(publisher).contextWrite(contextView) : publisher);
7071
}
7172

7273
if (value instanceof Flux) {
@@ -84,29 +85,48 @@ public Object get(DataFetchingEnvironment environment) throws Exception {
8485
return value;
8586
}
8687

88+
@SuppressWarnings("unchecked")
89+
private Publisher<?> interceptSubscriptionPublisherWithExceptionHandler(Publisher<?> publisher) {
90+
Function<? super Throwable, Mono<?>> onErrorResumeFunction = e ->
91+
subscriptionExceptionResolver.resolveException(e)
92+
.flatMap(errors -> Mono.error(new SubscriptionStreamException(errors)));
93+
94+
if (publisher instanceof Flux) {
95+
return ((Flux<Object>) publisher).onErrorResume(onErrorResumeFunction);
96+
}
97+
98+
if (publisher instanceof Mono) {
99+
return ((Mono<Object>) publisher).onErrorResume(onErrorResumeFunction);
100+
}
101+
102+
throw new IllegalArgumentException("Unknown publisher type: '" + publisher.getClass().getName() +"'. " +
103+
"Expected reactor.core.publisher.Mono or reactor.core.publisher.Flux");
104+
}
105+
87106
/**
88107
* {@link GraphQLTypeVisitor} that wraps non-GraphQL data fetchers and adapts them if
89108
* they return {@link Flux} or {@link Mono}.
90109
*/
91-
static GraphQLTypeVisitor TYPE_VISITOR = new GraphQLTypeVisitorStub() {
92-
93-
@Override
94-
public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition fieldDefinition,
95-
TraverserContext<GraphQLSchemaElement> context) {
96-
97-
GraphQLCodeRegistry.Builder codeRegistry = context.getVarFromParents(GraphQLCodeRegistry.Builder.class);
98-
GraphQLFieldsContainer parent = (GraphQLFieldsContainer) context.getParentNode();
99-
DataFetcher<?> dataFetcher = codeRegistry.getDataFetcher(parent, fieldDefinition);
100-
101-
if (dataFetcher.getClass().getPackage().getName().startsWith("graphql.")) {
110+
static GraphQLTypeVisitor createVisitor(SubscriptionExceptionResolver subscriptionExceptionResolver) {
111+
return new GraphQLTypeVisitorStub() {
112+
@Override
113+
public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition fieldDefinition,
114+
TraverserContext<GraphQLSchemaElement> context) {
115+
116+
GraphQLCodeRegistry.Builder codeRegistry = context.getVarFromParents(GraphQLCodeRegistry.Builder.class);
117+
GraphQLFieldsContainer parent = (GraphQLFieldsContainer) context.getParentNode();
118+
DataFetcher<?> dataFetcher = codeRegistry.getDataFetcher(parent, fieldDefinition);
119+
120+
if (dataFetcher.getClass().getPackage().getName().startsWith("graphql.")) {
121+
return TraversalControl.CONTINUE;
122+
}
123+
124+
boolean handlesSubscription = parent.getName().equals("Subscription");
125+
dataFetcher = new ContextDataFetcherDecorator(dataFetcher, handlesSubscription, subscriptionExceptionResolver);
126+
codeRegistry.dataFetcher(parent, fieldDefinition, dataFetcher);
102127
return TraversalControl.CONTINUE;
103128
}
104-
105-
boolean handlesSubscription = parent.getName().equals("Subscription");
106-
dataFetcher = new ContextDataFetcherDecorator(dataFetcher, handlesSubscription);
107-
codeRegistry.dataFetcher(parent, fieldDefinition, dataFetcher);
108-
return TraversalControl.CONTINUE;
109-
}
110-
};
129+
};
130+
}
111131

112132
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2002-2022 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+
17+
package org.springframework.graphql.execution;
18+
19+
import graphql.ErrorType;
20+
import graphql.GraphQLError;
21+
import graphql.GraphqlErrorBuilder;
22+
import org.apache.commons.logging.Log;
23+
import org.apache.commons.logging.LogFactory;
24+
import org.springframework.util.Assert;
25+
import reactor.core.publisher.Flux;
26+
import reactor.core.publisher.Mono;
27+
28+
import java.util.Collections;
29+
import java.util.List;
30+
31+
/**
32+
* An implementation of {@link SubscriptionExceptionResolver} that is trying to map exception to GraphQL error
33+
* using provided implementation of {@link SubscriptionExceptionResolver}.
34+
* <br/>
35+
* If none of provided implementations resolve exception to error or if any of implementation throw an exception,
36+
* this {@link SubscriptionExceptionResolver} will return a default error.
37+
*
38+
* @author Mykyta Ivchenko
39+
* @see SubscriptionExceptionResolver
40+
*/
41+
public class DelegatingSubscriptionExceptionResolver implements SubscriptionExceptionResolver {
42+
private static final Log logger = LogFactory.getLog(DelegatingSubscriptionExceptionResolver.class);
43+
private final List<SubscriptionExceptionResolver> resolvers;
44+
45+
public DelegatingSubscriptionExceptionResolver(List<SubscriptionExceptionResolver> resolvers) {
46+
Assert.notNull(resolvers, "'resolvers' list must be not null.");
47+
this.resolvers = resolvers;
48+
}
49+
50+
@Override
51+
public Mono<List<GraphQLError>> resolveException(Throwable exception) {
52+
return Flux.fromIterable(resolvers)
53+
.flatMap(resolver -> resolver.resolveException(exception))
54+
.next()
55+
.onErrorResume(error -> Mono.just(handleMappingException(error, exception)))
56+
.defaultIfEmpty(createDefaultErrors());
57+
}
58+
59+
private List<GraphQLError> handleMappingException(Throwable resolverException, Throwable originalException) {
60+
if (logger.isWarnEnabled()) {
61+
logger.warn("Failure while resolving " + originalException.getClass().getName(), resolverException);
62+
}
63+
return createDefaultErrors();
64+
}
65+
66+
private List<GraphQLError> createDefaultErrors() {
67+
GraphQLError error = GraphqlErrorBuilder.newError()
68+
.message("Unknown error")
69+
.errorType(ErrorType.DataFetchingException)
70+
.build();
71+
72+
return Collections.singletonList(error);
73+
}
74+
}

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,20 @@
1616

1717
package org.springframework.graphql.execution;
1818

19-
import java.io.InputStream;
20-
import java.util.List;
21-
import java.util.function.BiFunction;
22-
import java.util.function.Consumer;
23-
2419
import graphql.GraphQL;
2520
import graphql.execution.instrumentation.Instrumentation;
2621
import graphql.schema.GraphQLSchema;
2722
import graphql.schema.GraphQLTypeVisitor;
2823
import graphql.schema.TypeResolver;
2924
import graphql.schema.idl.RuntimeWiring;
3025
import graphql.schema.idl.TypeDefinitionRegistry;
31-
3226
import org.springframework.core.io.Resource;
3327

28+
import java.io.InputStream;
29+
import java.util.List;
30+
import java.util.function.BiFunction;
31+
import java.util.function.Consumer;
32+
3433

3534
/**
3635
* Strategy to resolve a {@link GraphQL} and a {@link GraphQLSchema}.
@@ -91,6 +90,14 @@ interface Builder<B extends Builder<B>> {
9190
*/
9291
B exceptionResolvers(List<DataFetcherExceptionResolver> resolvers);
9392

93+
/**
94+
* Add {@link SubscriptionExceptionResolver}s to map exceptions, thrown by
95+
* GraphQL Subscription publisher.
96+
* @param subscriptionExceptionResolver the subscription exception resolver
97+
* @return the current builder
98+
*/
99+
B subscriptionExceptionResolvers(List<SubscriptionExceptionResolver> subscriptionExceptionResolvers);
100+
94101
/**
95102
* Add {@link GraphQLTypeVisitor}s to visit all element of the created
96103
* {@link graphql.schema.GraphQLSchema}.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2002-2022 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+
17+
package org.springframework.graphql.execution;
18+
19+
import graphql.GraphQLError;
20+
import reactor.core.publisher.Mono;
21+
22+
import java.util.List;
23+
24+
/**
25+
* Contract to resolve exceptions, that are thrown by subscription publisher.
26+
* Implementations are typically declared as beans in Spring configuration and
27+
* are invoked sequentially until one emits a List of {@link GraphQLError}s.
28+
* <br/>
29+
* Usually, it is enough to implement this interface by extending {@link SubscriptionExceptionResolverAdapter}
30+
* and overriding one of its {@link SubscriptionExceptionResolverAdapter#resolveToSingleError(Throwable)}
31+
* or {@link SubscriptionExceptionResolverAdapter#resolveToMultipleErrors(Throwable)}
32+
*
33+
* @author Mykyta Ivchenko
34+
* @see SubscriptionExceptionResolverAdapter
35+
* @see DelegatingSubscriptionExceptionResolver
36+
* @see org.springframework.graphql.server.webflux.GraphQlWebSocketHandler
37+
*/
38+
@FunctionalInterface
39+
public interface SubscriptionExceptionResolver {
40+
/**
41+
* Resolve given exception as list of {@link GraphQLError}s and send them as WebSocket message.
42+
* @param exception the exception to resolve
43+
* @return a {@code Mono} with errors to send in a WebSocket message;
44+
* if the {@code Mono} completes with an empty List, the exception is resolved
45+
* without any errors added to the response; if the {@code Mono} completes
46+
* empty, without emitting a List, the exception remains unresolved and gives
47+
* other resolvers a chance.
48+
*/
49+
Mono<List<GraphQLError>> resolveException(Throwable exception);
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2002-2022 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+
17+
package org.springframework.graphql.execution;
18+
19+
import graphql.GraphQLError;
20+
import reactor.core.publisher.Mono;
21+
22+
import java.util.Collections;
23+
import java.util.List;
24+
25+
/**
26+
* Abstract class for {@link SubscriptionExceptionResolver} implementations.
27+
* This class provide an easy way to map an exception as GraphQL error synchronously.
28+
* <br/>
29+
* To use this class, you need to override either {@link SubscriptionExceptionResolverAdapter#resolveToSingleError(Throwable)}
30+
* or {@link SubscriptionExceptionResolverAdapter#resolveToMultipleErrors(Throwable)}.
31+
*
32+
* @author Mykyta Ivchenko
33+
* @see SubscriptionExceptionResolver
34+
*/
35+
public abstract class SubscriptionExceptionResolverAdapter implements SubscriptionExceptionResolver {
36+
@Override
37+
public Mono<List<GraphQLError>> resolveException(Throwable exception) {
38+
return Mono.just(resolveToMultipleErrors(exception));
39+
}
40+
41+
protected List<GraphQLError> resolveToMultipleErrors(Throwable exception) {
42+
return Collections.singletonList(resolveToSingleError(exception));
43+
}
44+
45+
protected GraphQLError resolveToSingleError(Throwable exception) {
46+
return null;
47+
}
48+
}

0 commit comments

Comments
 (0)