Skip to content

Commit 9b9a74c

Browse files
committed
Spec compliance: errors in buildExecutionContext() are caught and included in result rather than thrown
1 parent 678cf5d commit 9b9a74c

File tree

4 files changed

+290
-205
lines changed

4 files changed

+290
-205
lines changed

src/Executor/Executor.php

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -115,17 +115,26 @@ public static function execute(
115115
);
116116
}
117117

118-
$exeContext = self::buildExecutionContext(
119-
$schema,
120-
$ast,
121-
$rootValue,
122-
$contextValue,
123-
$variableValues,
124-
$operationName,
125-
$fieldResolver
126-
);
127118
$promiseAdapter = self::getPromiseAdapter();
128119

120+
try {
121+
$exeContext = self::buildExecutionContext(
122+
$schema,
123+
$ast,
124+
$rootValue,
125+
$contextValue,
126+
$variableValues,
127+
$operationName,
128+
$fieldResolver
129+
);
130+
} catch (Error $e) {
131+
if ($promiseAdapter instanceof SyncPromiseAdapter) {
132+
return new ExecutionResult(null, [$e]);
133+
} else {
134+
return $promiseAdapter->createFulfilled(new ExecutionResult(null, [$e]));
135+
}
136+
}
137+
129138
$executor = new self($exeContext, $promiseAdapter);
130139
$result = $executor->executeQuery();
131140

@@ -283,11 +292,30 @@ private function executeOperation(OperationDefinitionNode $operation, $rootValue
283292
$fields = $this->collectFields($type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
284293

285294
$path = [];
286-
if ($operation->operation === 'mutation') {
287-
return $this->executeFieldsSerially($type, $rootValue, $path, $fields);
288-
}
289295

290-
return $this->executeFields($type, $rootValue, $path, $fields);
296+
// Errors from sub-fields of a NonNull type may propagate to the top level,
297+
// at which point we still log the error and null the parent field, which
298+
// in this case is the entire response.
299+
//
300+
// Similar to completeValueCatchingError.
301+
try {
302+
$result = $operation->operation === 'mutation' ?
303+
$this->executeFieldsSerially($type, $rootValue, $path, $fields) :
304+
$this->executeFields($type, $rootValue, $path, $fields);
305+
306+
$promise = $this->getPromise($result);
307+
if ($promise) {
308+
return $promise->then(null, function($error) {
309+
$this->exeContext->addError($error);
310+
return null;
311+
});
312+
}
313+
return $result;
314+
315+
} catch (Error $error) {
316+
$this->exeContext->addError($error);
317+
return null;
318+
}
291319
}
292320

293321

tests/Executor/ExecutorTest.php

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -589,9 +589,9 @@ public function testUsesTheNamedOperationIfOperationNameIsProvided()
589589
}
590590

591591
/**
592-
* @it throws if no operation is provided
592+
* @it provides error if no operation is provided
593593
*/
594-
public function testThrowsIfNoOperationIsProvided()
594+
public function testProvidesErrorIfNoOperationIsProvided()
595595
{
596596
$doc = 'fragment Example on Type { a }';
597597
$data = [ 'a' => 'b' ];
@@ -605,62 +605,84 @@ public function testThrowsIfNoOperationIsProvided()
605605
])
606606
]);
607607

608-
try {
609-
Executor::execute($schema, $ast, $data);
610-
$this->fail('Expected exception was not thrown');
611-
} catch (Error $e) {
612-
$this->assertEquals('Must provide an operation.', $e->getMessage());
613-
}
608+
$result = Executor::execute($schema, $ast, $data);
609+
$expected = [
610+
'errors' => [
611+
[
612+
'message' => 'Must provide an operation.',
613+
]
614+
]
615+
];
616+
617+
$this->assertEquals($expected, $result->toArray());
614618
}
615619

616620
/**
617-
* @it throws if no operation name is provided with multiple operations
621+
* @it errors if no op name is provided with multiple operations
618622
*/
619-
public function testThrowsIfNoOperationIsProvidedWithMultipleOperations()
623+
public function testErrorsIfNoOperationIsProvidedWithMultipleOperations()
620624
{
621625
$doc = 'query Example { a } query OtherExample { a }';
622-
$data = [ 'a' => 'b' ];
626+
$data = ['a' => 'b'];
623627
$ast = Parser::parse($doc);
624628
$schema = new Schema([
625629
'query' => new ObjectType([
626630
'name' => 'Type',
627631
'fields' => [
628-
'a' => [ 'type' => Type::string() ],
632+
'a' => ['type' => Type::string()],
629633
]
630634
])
631635
]);
632636

633-
try {
634-
Executor::execute($schema, $ast, $data);
635-
$this->fail('Expected exception is not thrown');
636-
} catch (Error $err) {
637-
$this->assertEquals('Must provide operation name if query contains multiple operations.', $err->getMessage());
638-
}
637+
$result = Executor::execute($schema, $ast, $data);
638+
639+
$expected = [
640+
'errors' => [
641+
[
642+
'message' => 'Must provide operation name if query contains multiple operations.',
643+
]
644+
]
645+
];
646+
647+
$this->assertEquals($expected, $result->toArray());
639648
}
640649

641650
/**
642-
* @it throws if unknown operation name is provided
651+
* @it errors if unknown operation name is provided
643652
*/
644-
public function testThrowsIfUnknownOperationNameIsProvided()
653+
public function testErrorsIfUnknownOperationNameIsProvided()
645654
{
646655
$doc = 'query Example { a } query OtherExample { a }';
647-
$data = [ 'a' => 'b' ];
648656
$ast = Parser::parse($doc);
649657
$schema = new Schema([
650658
'query' => new ObjectType([
651659
'name' => 'Type',
652660
'fields' => [
653-
'a' => [ 'type' => Type::string() ],
661+
'a' => ['type' => Type::string()],
654662
]
655663
])
656664
]);
657665

658-
try {
659-
Executor::execute($schema, $ast, $data, null, null, 'UnknownExample');
660-
$this->fail('Expected exception was not thrown');
661-
} catch (Error $e) {
662-
$this->assertEquals('Unknown operation named "UnknownExample".', $e->getMessage());
663-
}
666+
667+
$result = Executor::execute(
668+
$schema,
669+
$ast,
670+
null,
671+
null,
672+
null,
673+
'UnknownExample'
674+
);
675+
676+
$expected = [
677+
'errors' => [
678+
[
679+
'message' => 'Unknown operation named "UnknownExample".',
680+
]
681+
]
682+
683+
];
684+
685+
$this->assertEquals($expected, $result->toArray());
664686
}
665687

666688
/**
@@ -956,20 +978,24 @@ public function testFailsToExecuteQueryContainingTypeDefinition()
956978
'query' => new ObjectType([
957979
'name' => 'Query',
958980
'fields' => [
959-
'foo' => ['type' => Type::string() ]
981+
'foo' => ['type' => Type::string()]
960982
]
961983
])
962984
]);
963985

964-
try {
965-
Executor::execute($schema, $query);
966-
$this->fail('Expected exception was not thrown');
967-
} catch (Error $e) {
968-
$this->assertEquals([
969-
'message' => 'GraphQL cannot execute a request containing a ObjectTypeDefinition.',
970-
'locations' => [['line' => 4, 'column' => 7]]
971-
], $e->toSerializableArray());
972-
}
986+
987+
$result = Executor::execute($schema, $query);
988+
989+
$expected = [
990+
'errors' => [
991+
[
992+
'message' => 'GraphQL cannot execute a request containing a ObjectTypeDefinition.',
993+
'locations' => [['line' => 4, 'column' => 7]],
994+
]
995+
]
996+
];
997+
998+
$this->assertEquals($expected, $result->toArray());
973999
}
9741000

9751001
/**

0 commit comments

Comments
 (0)