Skip to content

Commit 4213a30

Browse files
authored
Makes a default data fetcher exception handler possible (#2093)
1 parent f6f9bae commit 4213a30

3 files changed

Lines changed: 72 additions & 90 deletions

File tree

src/main/java/graphql/GraphQL.java

Lines changed: 43 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import graphql.execution.AbortExecutionException;
44
import graphql.execution.AsyncExecutionStrategy;
55
import graphql.execution.AsyncSerialExecutionStrategy;
6+
import graphql.execution.DataFetcherExceptionHandler;
67
import graphql.execution.Execution;
78
import graphql.execution.ExecutionId;
89
import graphql.execution.ExecutionIdProvider;
910
import graphql.execution.ExecutionStrategy;
11+
import graphql.execution.SimpleDataFetcherExceptionHandler;
1012
import graphql.execution.SubscriptionExecutionStrategy;
1113
import graphql.execution.ValueUnboxer;
1214
import graphql.execution.instrumentation.ChainedInstrumentation;
@@ -87,12 +89,6 @@ public class GraphQL {
8789
private static final Logger log = LoggerFactory.getLogger(GraphQL.class);
8890
private static final Logger logNotSafe = LogKit.getNotPrivacySafeLogger(GraphQL.class);
8991

90-
private final static Instrumentation DEFAULT_INSTRUMENTATION = new DataLoaderDispatcherInstrumentation();
91-
private final static ExecutionStrategy DEFAULT_QUERY_STRATEGY = new AsyncExecutionStrategy();
92-
private final static ExecutionStrategy DEFAULT_MUTATION_STRATEGY = new AsyncSerialExecutionStrategy();
93-
private final static ExecutionStrategy DEFAULT_SUBSCRIPTION_STRATEGY = new SubscriptionExecutionStrategy();
94-
95-
9692
private final GraphQLSchema graphQLSchema;
9793
private final ExecutionStrategy queryStrategy;
9894
private final ExecutionStrategy mutationStrategy;
@@ -103,76 +99,15 @@ public class GraphQL {
10399
private final ValueUnboxer valueUnboxer;
104100

105101

106-
/**
107-
* A GraphQL object ready to execute queries
108-
*
109-
* @param graphQLSchema the schema to use
110-
* @deprecated use the {@link #newGraphQL(GraphQLSchema)} builder instead. This will be removed in a future version.
111-
*/
112-
@Internal
113-
@Deprecated
114-
public GraphQL(GraphQLSchema graphQLSchema) {
115-
this(graphQLSchema, null, null);
116-
}
117-
118-
/**
119-
* A GraphQL object ready to execute queries
120-
*
121-
* @param graphQLSchema the schema to use
122-
* @param queryStrategy the query execution strategy to use
123-
* @deprecated use the {@link #newGraphQL(GraphQLSchema)} builder instead. This will be removed in a future version.
124-
*/
125-
@Internal
126-
@Deprecated
127-
public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy) {
128-
this(graphQLSchema, queryStrategy, null);
129-
}
130-
131-
/**
132-
* A GraphQL object ready to execute queries
133-
*
134-
* @param graphQLSchema the schema to use
135-
* @param queryStrategy the query execution strategy to use
136-
* @param mutationStrategy the mutation execution strategy to use
137-
* @deprecated use the {@link #newGraphQL(GraphQLSchema)} builder instead. This will be removed in a future version.
138-
*/
139-
@Internal
140-
@Deprecated
141-
public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy) {
142-
this(graphQLSchema, queryStrategy, mutationStrategy, null, DEFAULT_EXECUTION_ID_PROVIDER, DEFAULT_INSTRUMENTATION, NoOpPreparsedDocumentProvider.INSTANCE, ValueUnboxer.DEFAULT);
143-
}
144-
145-
/**
146-
* A GraphQL object ready to execute queries
147-
*
148-
* @param graphQLSchema the schema to use
149-
* @param queryStrategy the query execution strategy to use
150-
* @param mutationStrategy the mutation execution strategy to use
151-
* @param subscriptionStrategy the subscription execution strategy to use
152-
* @deprecated use the {@link #newGraphQL(GraphQLSchema)} builder instead. This will be removed in a future version.
153-
*/
154-
@Internal
155-
@Deprecated
156-
public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy, ExecutionStrategy subscriptionStrategy) {
157-
this(graphQLSchema, queryStrategy, mutationStrategy, subscriptionStrategy, DEFAULT_EXECUTION_ID_PROVIDER, DEFAULT_INSTRUMENTATION, NoOpPreparsedDocumentProvider.INSTANCE, ValueUnboxer.DEFAULT);
158-
}
159-
160-
private GraphQL(GraphQLSchema graphQLSchema,
161-
ExecutionStrategy queryStrategy,
162-
ExecutionStrategy mutationStrategy,
163-
ExecutionStrategy subscriptionStrategy,
164-
ExecutionIdProvider idProvider,
165-
Instrumentation instrumentation,
166-
PreparsedDocumentProvider preparsedDocumentProvider,
167-
ValueUnboxer valueUnboxer) {
168-
this.graphQLSchema = assertNotNull(graphQLSchema, () -> "graphQLSchema must be non null");
169-
this.queryStrategy = queryStrategy != null ? queryStrategy : DEFAULT_QUERY_STRATEGY;
170-
this.mutationStrategy = mutationStrategy != null ? mutationStrategy : DEFAULT_MUTATION_STRATEGY;
171-
this.subscriptionStrategy = subscriptionStrategy != null ? subscriptionStrategy : DEFAULT_SUBSCRIPTION_STRATEGY;
172-
this.idProvider = assertNotNull(idProvider, () -> "idProvider must be non null");
173-
this.instrumentation = assertNotNull(instrumentation);
174-
this.preparsedDocumentProvider = assertNotNull(preparsedDocumentProvider, () -> "preparsedDocumentProvider must be non null");
175-
this.valueUnboxer = valueUnboxer;
102+
private GraphQL(Builder builder) {
103+
this.graphQLSchema = assertNotNull(builder.graphQLSchema, () -> "graphQLSchema must be non null");
104+
this.queryStrategy = assertNotNull(builder.queryExecutionStrategy, () -> "queryStrategy must not be null");
105+
this.mutationStrategy = assertNotNull(builder.mutationExecutionStrategy, () -> "mutationStrategy must not be null");
106+
this.subscriptionStrategy = assertNotNull(builder.subscriptionExecutionStrategy, () -> "subscriptionStrategy must not be null");
107+
this.idProvider = assertNotNull(builder.idProvider, () -> "idProvider must be non null");
108+
this.instrumentation = assertNotNull(builder.instrumentation, () -> "instrumentation must not be null");
109+
this.preparsedDocumentProvider = assertNotNull(builder.preparsedDocumentProvider, () -> "preparsedDocumentProvider must be non null");
110+
this.valueUnboxer = assertNotNull(builder.valueUnboxer, () -> "valueUnboxer must not be null");
176111
}
177112

178113
/**
@@ -195,9 +130,9 @@ public static Builder newGraphQL(GraphQLSchema graphQLSchema) {
195130
public GraphQL transform(Consumer<GraphQL.Builder> builderConsumer) {
196131
Builder builder = new Builder(this.graphQLSchema);
197132
builder
198-
.queryExecutionStrategy(Optional.ofNullable(this.queryStrategy).orElse(builder.queryExecutionStrategy))
199-
.mutationExecutionStrategy(Optional.ofNullable(this.mutationStrategy).orElse(builder.mutationExecutionStrategy))
200-
.subscriptionExecutionStrategy(Optional.ofNullable(this.subscriptionStrategy).orElse(builder.subscriptionExecutionStrategy))
133+
.queryExecutionStrategy(this.queryStrategy)
134+
.mutationExecutionStrategy(this.mutationStrategy)
135+
.subscriptionExecutionStrategy(this.subscriptionStrategy)
201136
.executionIdProvider(Optional.ofNullable(this.idProvider).orElse(builder.idProvider))
202137
.instrumentation(Optional.ofNullable(this.instrumentation).orElse(builder.instrumentation))
203138
.preparsedDocumentProvider(Optional.ofNullable(this.preparsedDocumentProvider).orElse(builder.preparsedDocumentProvider));
@@ -210,9 +145,10 @@ public GraphQL transform(Consumer<GraphQL.Builder> builderConsumer) {
210145
@PublicApi
211146
public static class Builder {
212147
private GraphQLSchema graphQLSchema;
213-
private ExecutionStrategy queryExecutionStrategy = DEFAULT_QUERY_STRATEGY;
214-
private ExecutionStrategy mutationExecutionStrategy = DEFAULT_MUTATION_STRATEGY;
215-
private ExecutionStrategy subscriptionExecutionStrategy = DEFAULT_SUBSCRIPTION_STRATEGY;
148+
private ExecutionStrategy queryExecutionStrategy;
149+
private ExecutionStrategy mutationExecutionStrategy;
150+
private ExecutionStrategy subscriptionExecutionStrategy;
151+
private DataFetcherExceptionHandler defaultExceptionHandler = new SimpleDataFetcherExceptionHandler();
216152
private ExecutionIdProvider idProvider = DEFAULT_EXECUTION_ID_PROVIDER;
217153
private Instrumentation instrumentation = null; // deliberate default here
218154
private PreparsedDocumentProvider preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE;
@@ -244,6 +180,18 @@ public Builder subscriptionExecutionStrategy(ExecutionStrategy executionStrategy
244180
return this;
245181
}
246182

183+
/**
184+
* This allows you to set a default {@link graphql.execution.DataFetcherExceptionHandler} that will be used to handle exceptions that happen
185+
* in {@link graphql.schema.DataFetcher} invocations.
186+
*
187+
* @param dataFetcherExceptionHandler the default handler for data fetching exception
188+
* @return this builder
189+
*/
190+
public Builder defaultDataFetcherExceptionHandler(DataFetcherExceptionHandler dataFetcherExceptionHandler) {
191+
this.defaultExceptionHandler = assertNotNull(dataFetcherExceptionHandler, () -> "The DataFetcherExceptionHandler must be non null");
192+
return this;
193+
}
194+
247195
public Builder instrumentation(Instrumentation instrumentation) {
248196
this.instrumentation = assertNotNull(instrumentation, () -> "Instrumentation must be non null");
249197
return this;
@@ -282,11 +230,19 @@ public Builder valueUnboxer(ValueUnboxer valueUnboxer) {
282230
}
283231

284232
public GraphQL build() {
285-
assertNotNull(graphQLSchema, () -> "graphQLSchema must be non null");
286-
assertNotNull(queryExecutionStrategy, () -> "queryStrategy must be non null");
287-
assertNotNull(idProvider, () -> "idProvider must be non null");
288-
final Instrumentation augmentedInstrumentation = checkInstrumentationDefaultState(instrumentation, doNotAddDefaultInstrumentations);
289-
return new GraphQL(graphQLSchema, queryExecutionStrategy, mutationExecutionStrategy, subscriptionExecutionStrategy, idProvider, augmentedInstrumentation, preparsedDocumentProvider, valueUnboxer);
233+
// we use the data fetcher exception handler unless they set their own strategy in which case bets are off
234+
if (queryExecutionStrategy == null) {
235+
this.queryExecutionStrategy = new AsyncExecutionStrategy(this.defaultExceptionHandler);
236+
}
237+
if (mutationExecutionStrategy == null) {
238+
this.mutationExecutionStrategy = new AsyncSerialExecutionStrategy(this.defaultExceptionHandler);
239+
}
240+
if (subscriptionExecutionStrategy == null) {
241+
this.subscriptionExecutionStrategy = new SubscriptionExecutionStrategy(this.defaultExceptionHandler);
242+
}
243+
244+
this.instrumentation = checkInstrumentationDefaultState(this.instrumentation, this.doNotAddDefaultInstrumentations);
245+
return new GraphQL(this);
290246
}
291247
}
292248

src/main/java/graphql/execution/ValueUnboxer.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22

33
import graphql.PublicSpi;
44

5+
/**
6+
* A value unboxer takes values that are wrapped in classes like {@link java.util.Optional} / {@link java.util.OptionalInt} etc..
7+
* and returns value from them. You can provide your own implementation if you have your own specific
8+
* holder classes.
9+
*/
510
@PublicSpi
611
public interface ValueUnboxer {
712

13+
/**
14+
* The default value unboxer handles JDK classes such as {@link java.util.Optional} and {@link java.util.OptionalInt} etc..
15+
*/
816
ValueUnboxer DEFAULT = new DefaultValueUnboxer();
917

1018
/**
@@ -13,7 +21,6 @@ public interface ValueUnboxer {
1321
* unmodified
1422
*
1523
* @param object to unbox
16-
*
1724
* @return unboxed object, or original if cannot unbox
1825
*/
1926
Object unbox(final Object object);

src/test/groovy/graphql/GraphQLTest.groovy

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package graphql
33
import graphql.analysis.MaxQueryComplexityInstrumentation
44
import graphql.analysis.MaxQueryDepthInstrumentation
55
import graphql.execution.AsyncExecutionStrategy
6+
import graphql.execution.DataFetcherExceptionHandler
67
import graphql.execution.DataFetcherResult
78
import graphql.execution.ExecutionContext
89
import graphql.execution.ExecutionId
@@ -266,7 +267,7 @@ class GraphQLTest extends Specification {
266267
.build()
267268

268269
when:
269-
def result = new GraphQL(schema).execute("mutation { doesNotExist }")
270+
def result = GraphQL.newGraphQL(schema).build().execute("mutation { doesNotExist }")
270271

271272
then:
272273
result.errors.size() == 1
@@ -283,7 +284,7 @@ class GraphQLTest extends Specification {
283284
.build()
284285

285286
when:
286-
def result = new GraphQL(schema).execute("subscription { doesNotExist }")
287+
def result = GraphQL.newGraphQL(schema).build().execute("subscription { doesNotExist }")
287288

288289
then:
289290
result.errors.size() == 1
@@ -1170,4 +1171,22 @@ many lines''']
11701171
then:
11711172
er.data["f"] == "hi"
11721173
}
1174+
1175+
def "can set default fetcher exception handler"() {
1176+
def sdl = 'type Query { f : String } '
1177+
1178+
DataFetcher df = { env ->
1179+
throw new RuntimeException("BANG!")
1180+
}
1181+
def capturedMsg = null
1182+
def exceptionHandler = { params ->
1183+
capturedMsg = params.exception.getMessage()
1184+
} as DataFetcherExceptionHandler
1185+
def schema = TestUtil.schema(sdl, [Query: [f: df]])
1186+
def graphQL = GraphQL.newGraphQL(schema).defaultDataFetcherExceptionHandler(exceptionHandler).build()
1187+
when:
1188+
graphQL.execute("{f}")
1189+
then:
1190+
capturedMsg == "BANG!"
1191+
}
11731192
}

0 commit comments

Comments
 (0)