Skip to content

Commit a5809e9

Browse files
committed
Polishing contribution
See gh-398
1 parent a7e68d3 commit a5809e9

22 files changed

+440
-392
lines changed

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
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+
1925
import graphql.GraphQL;
2026
import graphql.execution.instrumentation.ChainedInstrumentation;
2127
import graphql.execution.instrumentation.Instrumentation;
@@ -24,9 +30,6 @@
2430
import graphql.schema.GraphQLTypeVisitor;
2531
import graphql.schema.SchemaTraverser;
2632

27-
import java.util.*;
28-
import java.util.function.Consumer;
29-
3033

3134
/**
3235
* Implementation of {@link GraphQlSource.Builder} that leaves it to subclasses
@@ -57,8 +60,8 @@ public B exceptionResolvers(List<DataFetcherExceptionResolver> resolvers) {
5760
}
5861

5962
@Override
60-
public B subscriptionExceptionResolvers(List<SubscriptionExceptionResolver> subscriptionExceptionResolvers) {
61-
this.subscriptionExceptionResolvers.addAll(subscriptionExceptionResolvers);
63+
public B subscriptionExceptionResolvers(List<SubscriptionExceptionResolver> resolvers) {
64+
this.subscriptionExceptionResolvers.addAll(resolvers);
6265
return self();
6366
}
6467

@@ -110,10 +113,7 @@ public GraphQlSource build() {
110113
protected abstract GraphQLSchema initGraphQlSchema();
111114

112115
private GraphQLSchema applyTypeVisitors(GraphQLSchema schema) {
113-
SubscriptionExceptionResolver subscriptionExceptionResolver = new DelegatingSubscriptionExceptionResolver(
114-
subscriptionExceptionResolvers);
115-
GraphQLTypeVisitor visitor = ContextDataFetcherDecorator.createVisitor(subscriptionExceptionResolver);
116-
116+
GraphQLTypeVisitor visitor = ContextDataFetcherDecorator.createVisitor(this.subscriptionExceptionResolvers);
117117
List<GraphQLTypeVisitor> visitors = new ArrayList<>(this.typeVisitors);
118118
visitors.add(visitor);
119119

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,59 +16,66 @@
1616

1717
package org.springframework.graphql.execution;
1818

19-
import graphql.ErrorType;
19+
import java.util.Collections;
20+
import java.util.List;
21+
2022
import graphql.GraphQLError;
2123
import graphql.GraphqlErrorBuilder;
2224
import org.apache.commons.logging.Log;
2325
import org.apache.commons.logging.LogFactory;
24-
import org.springframework.util.Assert;
2526
import reactor.core.publisher.Flux;
2627
import reactor.core.publisher.Mono;
2728

28-
import java.util.Collections;
29-
import java.util.List;
29+
import org.springframework.util.Assert;
3030

3131
/**
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.
32+
* Implementation of {@link SubscriptionExceptionResolver} that is given a list
33+
* of other {@link SubscriptionExceptionResolver}'s to invoke in turn until one
34+
* returns a list of {@link GraphQLError}'s.
35+
*
36+
* <p>If the exception remains unresolved, it is mapped to a default error of
37+
* type {@link ErrorType#INTERNAL_ERROR} with a generic message.
3738
*
3839
* @author Mykyta Ivchenko
39-
* @see SubscriptionExceptionResolver
40+
* @author Rossen Stoyanchev
41+
* @since 1.0.1
4042
*/
41-
public class DelegatingSubscriptionExceptionResolver implements SubscriptionExceptionResolver {
42-
private static final Log logger = LogFactory.getLog(DelegatingSubscriptionExceptionResolver.class);
43+
class CompositeSubscriptionExceptionResolver implements SubscriptionExceptionResolver {
44+
45+
private static final Log logger = LogFactory.getLog(CompositeSubscriptionExceptionResolver.class);
46+
4347
private final List<SubscriptionExceptionResolver> resolvers;
4448

45-
public DelegatingSubscriptionExceptionResolver(List<SubscriptionExceptionResolver> resolvers) {
46-
Assert.notNull(resolvers, "'resolvers' list must be not null.");
49+
50+
CompositeSubscriptionExceptionResolver(List<SubscriptionExceptionResolver> resolvers) {
51+
Assert.notNull(resolvers, "'resolvers' is required");
4752
this.resolvers = resolvers;
4853
}
4954

55+
5056
@Override
5157
public Mono<List<GraphQLError>> resolveException(Throwable exception) {
52-
return Flux.fromIterable(resolvers)
58+
return Flux.fromIterable(this.resolvers)
5359
.flatMap(resolver -> resolver.resolveException(exception))
5460
.next()
55-
.onErrorResume(error -> Mono.just(handleMappingException(error, exception)))
56-
.defaultIfEmpty(createDefaultErrors());
61+
.onErrorResume(error -> Mono.just(handleResolverException(error, exception)))
62+
.defaultIfEmpty(createDefaultError());
5763
}
5864

59-
private List<GraphQLError> handleMappingException(Throwable resolverException, Throwable originalException) {
65+
private List<GraphQLError> handleResolverException(
66+
Throwable resolverException, Throwable originalException) {
67+
6068
if (logger.isWarnEnabled()) {
6169
logger.warn("Failure while resolving " + originalException.getClass().getName(), resolverException);
6270
}
63-
return createDefaultErrors();
71+
return createDefaultError();
6472
}
6573

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);
74+
private List<GraphQLError> createDefaultError() {
75+
return Collections.singletonList(GraphqlErrorBuilder.newError()
76+
.message("Subscription error")
77+
.errorType(ErrorType.INTERNAL_ERROR)
78+
.build());
7379
}
80+
7481
}

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

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,25 @@
1616

1717
package org.springframework.graphql.execution;
1818

19+
import java.util.List;
20+
1921
import graphql.ExecutionInput;
20-
import graphql.schema.*;
22+
import graphql.schema.DataFetcher;
23+
import graphql.schema.DataFetchingEnvironment;
24+
import graphql.schema.GraphQLCodeRegistry;
25+
import graphql.schema.GraphQLFieldDefinition;
26+
import graphql.schema.GraphQLFieldsContainer;
27+
import graphql.schema.GraphQLSchemaElement;
28+
import graphql.schema.GraphQLTypeVisitor;
29+
import graphql.schema.GraphQLTypeVisitorStub;
2130
import graphql.util.TraversalControl;
2231
import graphql.util.TraverserContext;
2332
import org.reactivestreams.Publisher;
24-
import org.springframework.util.Assert;
2533
import reactor.core.publisher.Flux;
2634
import reactor.core.publisher.Mono;
2735
import reactor.util.context.ContextView;
2836

29-
import java.util.function.Function;
37+
import org.springframework.util.Assert;
3038

3139
/**
3240
* Wrap a {@link DataFetcher} to enable the following:
@@ -35,6 +43,7 @@
3543
* <li>Support {@link Flux} return value as a shortcut to {@link Flux#collectList()}.
3644
* <li>Re-establish Reactor Context passed via {@link ExecutionInput}.
3745
* <li>Re-establish ThreadLocal context passed via {@link ExecutionInput}.
46+
* <li>Resolve exceptions from a GraphQL subscription {@link Publisher}.
3847
* </ul>
3948
*
4049
* @author Rossen Stoyanchev
@@ -50,6 +59,7 @@ final class ContextDataFetcherDecorator implements DataFetcher<Object> {
5059
private ContextDataFetcherDecorator(
5160
DataFetcher<?> delegate, boolean subscription,
5261
SubscriptionExceptionResolver subscriptionExceptionResolver) {
62+
5363
Assert.notNull(delegate, "'delegate' DataFetcher is required");
5464
Assert.notNull(subscriptionExceptionResolver, "'subscriptionExceptionResolver' is required");
5565
this.delegate = delegate;
@@ -66,8 +76,11 @@ public Object get(DataFetchingEnvironment environment) throws Exception {
6676
ContextView contextView = ReactorContextManager.getReactorContext(environment.getGraphQlContext());
6777

6878
if (this.subscription) {
69-
Publisher<?> publisher = interceptSubscriptionPublisherWithExceptionHandler((Publisher<?>) value);
70-
return (!contextView.isEmpty() ? Flux.from(publisher).contextWrite(contextView) : publisher);
79+
Assert.state(value instanceof Publisher, "Expected Publisher for a subscription");
80+
Flux<?> flux = Flux.from((Publisher<?>) value).onErrorResume(exception ->
81+
this.subscriptionExceptionResolver.resolveException(exception)
82+
.flatMap(errors -> Mono.error(new SubscriptionPublisherException(errors, exception))));
83+
return (!contextView.isEmpty() ? flux.contextWrite(contextView) : flux);
7184
}
7285

7386
if (value instanceof Flux) {
@@ -85,29 +98,15 @@ public Object get(DataFetchingEnvironment environment) throws Exception {
8598
return value;
8699
}
87100

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-
}
105101

106102
/**
107-
* {@link GraphQLTypeVisitor} that wraps non-GraphQL data fetchers and adapts them if
108-
* they return {@link Flux} or {@link Mono}.
103+
* Static factory method to create {@link GraphQLTypeVisitor} that wraps
104+
* data fetchers with the {@link ContextDataFetcherDecorator}.
109105
*/
110-
static GraphQLTypeVisitor createVisitor(SubscriptionExceptionResolver subscriptionExceptionResolver) {
106+
static GraphQLTypeVisitor createVisitor(List<SubscriptionExceptionResolver> resolvers) {
107+
108+
SubscriptionExceptionResolver compositeResolver = new CompositeSubscriptionExceptionResolver(resolvers);
109+
111110
return new GraphQLTypeVisitorStub() {
112111
@Override
113112
public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition fieldDefinition,
@@ -122,7 +121,7 @@ public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition field
122121
}
123122

124123
boolean handlesSubscription = parent.getName().equals("Subscription");
125-
dataFetcher = new ContextDataFetcherDecorator(dataFetcher, handlesSubscription, subscriptionExceptionResolver);
124+
dataFetcher = new ContextDataFetcherDecorator(dataFetcher, handlesSubscription, compositeResolver);
126125
codeRegistry.dataFetcher(parent, fieldDefinition, dataFetcher);
127126
return TraversalControl.CONTINUE;
128127
}

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.graphql.execution;
1818

1919
import java.util.List;
20+
import java.util.function.BiFunction;
2021

2122
import graphql.GraphQLError;
2223
import graphql.schema.DataFetchingEnvironment;
@@ -55,9 +56,31 @@ public interface DataFetcherExceptionResolver {
5556
* @return a {@code Mono} with errors to add to the GraphQL response;
5657
* if the {@code Mono} completes with an empty List, the exception is resolved
5758
* without any errors added to the response; if the {@code Mono} completes
58-
* empty, without emitting a List, the exception remains unresolved and gives
59-
* other resolvers a chance.
59+
* empty, without emitting a List, the exception remains unresolved and that
60+
* allows other resolvers to resolve it.
6061
*/
6162
Mono<List<GraphQLError>> resolveException(Throwable exception, DataFetchingEnvironment environment);
6263

64+
65+
/**
66+
* Factory method to create a {@link DataFetcherExceptionResolver} to resolve
67+
* an exception to a single GraphQL error. Effectively, a shortcut
68+
* for creating {@link DataFetcherExceptionResolverAdapter} and overriding
69+
* its {@code resolveToSingleError} method.
70+
* @param resolver the resolver function to use
71+
* @return the created instance
72+
* @since 1.0.1
73+
*/
74+
static DataFetcherExceptionResolverAdapter forSingleError(
75+
BiFunction<Throwable, DataFetchingEnvironment, GraphQLError> resolver) {
76+
77+
return new DataFetcherExceptionResolverAdapter() {
78+
79+
@Override
80+
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
81+
return resolver.apply(ex, env);
82+
}
83+
};
84+
}
85+
6386
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,16 @@
3434
* <li>{@link #resolveToMultipleErrors}
3535
* </ul>
3636
*
37-
* <p>Use {@link #from(BiFunction)} to create an instance or extend this class
38-
* and override one of its resolve methods.
37+
* <p>Applications may also use
38+
* {@link DataFetcherExceptionResolver#forSingleError(BiFunction)} as a shortcut
39+
* for {@link #resolveToSingleError(Throwable, DataFetchingEnvironment)}.
3940
*
4041
* <p>Implementations can also express interest in ThreadLocal context
4142
* propagation, from the underlying transport thread, via
4243
* {@link #setThreadLocalContextAware(boolean)}.
4344
*
4445
* @author Rossen Stoyanchev
46+
* @since 1.0.0
4547
*/
4648
public abstract class DataFetcherExceptionResolverAdapter implements DataFetcherExceptionResolver {
4749

@@ -57,7 +59,7 @@ protected DataFetcherExceptionResolverAdapter() {
5759

5860

5961
/**
60-
* Sub-classes can set this to indicate that ThreadLocal context from the
62+
* Subclasses can set this to indicate that ThreadLocal context from the
6163
* transport handler (e.g. HTTP handler) should be restored when resolving
6264
* exceptions.
6365
* <p><strong>Note:</strong> This property is applicable only if transports
@@ -129,7 +131,9 @@ protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironmen
129131
* resolves exceptions with the given {@code BiFunction}.
130132
* @param resolver the resolver function to use
131133
* @return the created instance
134+
* @deprecated as of 1.0.1, please use {@link DataFetcherExceptionResolver#forSingleError(BiFunction)}
132135
*/
136+
@Deprecated
133137
public static DataFetcherExceptionResolverAdapter from(
134138
BiFunction<Throwable, DataFetchingEnvironment, GraphQLError> resolver) {
135139

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
* in a sequence until one returns a list of {@link GraphQLError}'s.
4242
*
4343
* @author Rossen Stoyanchev
44+
* @since 1.0.0
4445
*/
4546
class ExceptionResolversExceptionHandler implements DataFetcherExceptionHandler {
4647

0 commit comments

Comments
 (0)