Skip to content

Commit 653d67f

Browse files
tinnoubbakerman
authored andcommitted
Fix NPE in FieldLevelTrackingApproach when type mismatch error
1 parent 6abf5e8 commit 653d67f

File tree

5 files changed

+183
-1
lines changed

5 files changed

+183
-1
lines changed

src/main/java/graphql/execution/AsyncExecutionStrategy.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
8282
List<CompletableFuture<ExecutionResult>> executionResultFuture = completeValueInfos.stream().map(FieldValueInfo::getFieldValue).collect(Collectors.toList());
8383
executionStrategyCtx.onFieldValuesInfo(completeValueInfos);
8484
Async.each(executionResultFuture).whenComplete(handleResultsConsumer);
85+
}).exceptionally((ex) -> {
86+
// if there are any issues with combining/handling the field results,
87+
// complete the future at all costs and bubble up any thrown exception so
88+
// the execution does not hang.
89+
overallResult.completeExceptionally(ex);
90+
return null;
8591
});
8692

8793
overallResult.whenComplete(executionStrategyCtx::onCompleted);

src/main/java/graphql/execution/FieldValueInfo.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
import graphql.ExecutionResult;
44
import graphql.PublicApi;
55

6+
import java.util.ArrayList;
67
import java.util.List;
78
import java.util.concurrent.CompletableFuture;
89

10+
import static graphql.Assert.assertNotNull;
11+
912
@PublicApi
1013
public class FieldValueInfo {
1114

@@ -23,6 +26,7 @@ public enum CompleteValueType {
2326
private final List<FieldValueInfo> fieldValueInfos;
2427

2528
private FieldValueInfo(CompleteValueType completeValueType, CompletableFuture<ExecutionResult> fieldValue, List<FieldValueInfo> fieldValueInfos) {
29+
assertNotNull(fieldValueInfos, "fieldValueInfos can't be null");
2630
this.completeValueType = completeValueType;
2731
this.fieldValue = fieldValue;
2832
this.fieldValueInfos = fieldValueInfos;
@@ -57,7 +61,7 @@ public String toString() {
5761
public static class Builder {
5862
private CompleteValueType completeValueType;
5963
private CompletableFuture<ExecutionResult> executionResultFuture;
60-
private List<FieldValueInfo> listInfos;
64+
private List<FieldValueInfo> listInfos = new ArrayList<>();
6165

6266
public Builder(CompleteValueType completeValueType) {
6367
this.completeValueType = completeValueType;
@@ -74,6 +78,7 @@ public Builder fieldValue(CompletableFuture<ExecutionResult> executionResultFutu
7478
}
7579

7680
public Builder fieldValueInfos(List<FieldValueInfo> listInfos) {
81+
assertNotNull(listInfos, "fieldValueInfos can't be null");
7782
this.listInfos = listInfos;
7883
return this;
7984
}

src/test/groovy/graphql/execution/AsyncExecutionStrategyTest.groovy

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package graphql.execution
22

33
import graphql.ErrorType
4+
import graphql.ExecutionResult
5+
import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext
46
import graphql.execution.instrumentation.SimpleInstrumentation
7+
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters
58
import graphql.language.Field
69
import graphql.language.OperationDefinition
710
import graphql.parser.Parser
@@ -11,6 +14,7 @@ import graphql.schema.GraphQLSchema
1114
import spock.lang.Specification
1215

1316
import java.util.concurrent.CompletableFuture
17+
import java.util.concurrent.CompletionException
1418
import java.util.concurrent.atomic.AtomicInteger
1519
import java.util.concurrent.locks.ReentrantLock
1620

@@ -211,4 +215,67 @@ class AsyncExecutionStrategyTest extends Specification {
211215
result.get().getErrors().get(0).errorType == ErrorType.DataFetchingException
212216

213217
}
218+
219+
def "exception in instrumentation while combining data"() {
220+
GraphQLSchema schema = schema(
221+
{ env -> CompletableFuture.completedFuture("world") },
222+
{ env -> CompletableFuture.completedFuture("world2") }
223+
)
224+
String query = "{hello, hello2}"
225+
def document = new Parser().parseDocument(query)
226+
def operation = document.definitions[0] as OperationDefinition
227+
228+
def typeInfo = ExecutionTypeInfo.newTypeInfo()
229+
.type(schema.getQueryType())
230+
.build()
231+
232+
ExecutionContext executionContext = new ExecutionContextBuilder()
233+
.graphQLSchema(schema)
234+
.executionId(ExecutionId.generate())
235+
.operationDefinition(operation)
236+
.instrumentation(new SimpleInstrumentation() {
237+
@Override
238+
ExecutionStrategyInstrumentationContext beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) {
239+
return new ExecutionStrategyInstrumentationContext() {
240+
241+
@Override
242+
void onFieldValuesInfo(List<FieldValueInfo> fieldValueInfoList) {
243+
throw new RuntimeException("Exception raised from instrumentation")
244+
}
245+
246+
@Override
247+
public void onDispatched(CompletableFuture<ExecutionResult> result) {
248+
249+
}
250+
251+
@Override
252+
public void onCompleted(ExecutionResult result, Throwable t) {
253+
254+
}
255+
}
256+
}
257+
})
258+
.build()
259+
ExecutionStrategyParameters executionStrategyParameters = ExecutionStrategyParameters
260+
.newParameters()
261+
.typeInfo(typeInfo)
262+
.fields(['hello': [new Field('hello')], 'hello2': [new Field('hello2')]])
263+
.build()
264+
265+
AsyncExecutionStrategy asyncExecutionStrategy = new AsyncExecutionStrategy()
266+
when:
267+
def result = asyncExecutionStrategy.execute(executionContext, executionStrategyParameters)
268+
269+
then: "result should be completed"
270+
result.isCompletedExceptionally()
271+
272+
when:
273+
result.join()
274+
275+
then: "exceptions thrown from the instrumentation should be bubbled up"
276+
def ex = thrown(CompletionException)
277+
ex.cause.message == "Exception raised from instrumentation"
278+
}
279+
280+
214281
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package graphql.execution
2+
3+
import graphql.AssertException
4+
import spock.lang.Specification
5+
6+
7+
class FieldValueInfoTest extends Specification{
8+
9+
def "simple constructor test"() {
10+
when:
11+
def fieldValueInfo = FieldValueInfo.newFieldValueInfo().build()
12+
13+
then: "fieldValueInfos to be empty list"
14+
fieldValueInfo.fieldValueInfos == [] as List
15+
16+
and: "other fields to be null "
17+
fieldValueInfo.fieldValue == null
18+
fieldValueInfo.completeValueType == null
19+
}
20+
21+
def "negative constructor test"() {
22+
when:
23+
FieldValueInfo.newFieldValueInfo()
24+
.fieldValueInfos(null)
25+
.build()
26+
then:
27+
def assEx = thrown(AssertException)
28+
assEx.message.contains("fieldValueInfos")
29+
}
30+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package graphql.execution.instrumentation.dataloader
2+
3+
import graphql.GraphQL
4+
import graphql.schema.DataFetcher
5+
import graphql.schema.DataFetchingEnvironment
6+
import graphql.schema.idl.RuntimeWiring
7+
import graphql.schema.idl.SchemaGenerator
8+
import graphql.schema.idl.SchemaParser
9+
import org.dataloader.BatchLoader
10+
import org.dataloader.DataLoader
11+
import org.dataloader.DataLoaderRegistry
12+
import spock.lang.Specification
13+
14+
import java.util.concurrent.CompletableFuture
15+
import java.util.concurrent.CompletionStage
16+
17+
import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring
18+
19+
class DataLoaderTypeMismatchTest extends Specification {
20+
21+
def "when actual field value return type is different from expected return type, then it should not hang execution"() {
22+
setup:
23+
def sdl = """
24+
type Todo {
25+
id: ID!
26+
}
27+
28+
type Query {
29+
getTodos: [Todo]
30+
}
31+
32+
schema {
33+
query: Query
34+
}"""
35+
36+
def typeDefinitionRegistry = new SchemaParser().parse(sdl)
37+
38+
def dataLoader = new DataLoader<Object, Object>(new BatchLoader<Object, Object>() {
39+
@Override
40+
CompletionStage<List<Object>> load(List<Object> keys) {
41+
return CompletableFuture.completedFuture([
42+
[a: "map instead of a list of todos"]
43+
])
44+
}
45+
})
46+
def dataLoaderRegistry = new DataLoaderRegistry()
47+
dataLoaderRegistry.register("getTodos", dataLoader)
48+
49+
def todosDef = new DataFetcher<CompletableFuture<Object>>() {
50+
@Override
51+
CompletableFuture<Object> get(DataFetchingEnvironment environment) {
52+
return dataLoader.load(environment)
53+
}
54+
}
55+
56+
def wiring = RuntimeWiring.newRuntimeWiring()
57+
.type(newTypeWiring("Query")
58+
.dataFetcher("getTodos", todosDef))
59+
.build()
60+
61+
def schema = new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, wiring)
62+
63+
def graphql = GraphQL.newGraphQL(schema)
64+
.instrumentation(new DataLoaderDispatcherInstrumentation(dataLoaderRegistry))
65+
.build()
66+
67+
when:
68+
def result = graphql.execute("query { getTodos { id } }")
69+
70+
then: "execution shouldn't hang"
71+
!result.errors.empty
72+
result.errors[0].message == "Can't resolve value (/getTodos) : type mismatch error, expected type LIST"
73+
}
74+
}

0 commit comments

Comments
 (0)