diff --git a/src/Error/ClientAware.php b/src/Error/ClientAware.php index 26ab969..df69d14 100644 --- a/src/Error/ClientAware.php +++ b/src/Error/ClientAware.php @@ -14,4 +14,13 @@ interface ClientAware * @return bool */ public function isClientSafe(); + + /** + * Returns string describing error category. + * + * Value "graphql" is reserved for errors produced by query parsing or validation, do not use it. + * + * @return string + */ + public function getCategory(); } diff --git a/src/Error/Error.php b/src/Error/Error.php index 3677298..e2f3b81 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -16,6 +16,9 @@ use GraphQL\Utils\Utils; */ class Error extends \Exception implements \JsonSerializable, ClientAware { + const GRAPHQL = 'graphql'; + const INTERNAL = 'internal'; + /** * A message describing the Error for debugging purposes. * @@ -67,6 +70,11 @@ class Error extends \Exception implements \JsonSerializable, ClientAware */ private $isClientSafe; + /** + * @var string + */ + protected $category; + /** * Given an arbitrary Error, presumably thrown while attempting to execute a * GraphQL operation, produce a new GraphQLError aware of the location in the @@ -131,7 +139,14 @@ class Error extends \Exception implements \JsonSerializable, ClientAware * @param array|null $path * @param \Throwable $previous */ - public function __construct($message, $nodes = null, Source $source = null, $positions = null, $path = null, $previous = null) + public function __construct( + $message, + $nodes = null, + Source $source = null, + $positions = null, + $path = null, + $previous = null + ) { parent::__construct($message, 0, $previous); @@ -146,21 +161,32 @@ class Error extends \Exception implements \JsonSerializable, ClientAware if ($previous instanceof ClientAware) { $this->isClientSafe = $previous->isClientSafe(); - } else if ($previous === null) { - $this->isClientSafe = true; - } else { + $this->category = $previous->getCategory() ?: static::INTERNAL; + } else if ($previous) { $this->isClientSafe = false; + $this->category = static::INTERNAL; + } else { + $this->isClientSafe = true; + $this->category = static::GRAPHQL; } } /** - * @return bool + * @inheritdoc */ public function isClientSafe() { return $this->isClientSafe; } + /** + * @inheritdoc + */ + public function getCategory() + { + return $this->category; + } + /** * @return Source|null */ @@ -222,7 +248,7 @@ class Error extends \Exception implements \JsonSerializable, ClientAware public function toSerializableArray() { $arr = [ - 'message' => $this->getMessage(), + 'message' => $this->getMessage() ]; $locations = Utils::map($this->getLocations(), function(SourceLocation $loc) { diff --git a/src/Error/FormattedError.php b/src/Error/FormattedError.php index 42605ee..395216b 100644 --- a/src/Error/FormattedError.php +++ b/src/Error/FormattedError.php @@ -44,18 +44,19 @@ class FormattedError if ($e instanceof ClientAware) { if ($e->isClientSafe()) { $result = [ - 'message' => $e->getMessage() + 'message' => $e->getMessage(), + 'category' => $e->getCategory() ]; } else { $result = [ 'message' => $internalErrorMessage, - 'isInternalError' => true + 'category' => $e->getCategory() ]; } } else { $result = [ 'message' => $internalErrorMessage, - 'isInternalError' => true + 'category' => Error::INTERNAL ]; } if (($debug & self::INCLUDE_DEBUG_MESSAGE > 0) && $result['message'] === $internalErrorMessage) { diff --git a/src/Error/UserError.php b/src/Error/UserError.php index bf283a5..5a4b38c 100644 --- a/src/Error/UserError.php +++ b/src/Error/UserError.php @@ -4,7 +4,7 @@ namespace GraphQL\Error; /** * Class UserError * - * Error caused by actions of GraphQL clients. Can be safely displayed to client... + * Error caused by actions of GraphQL clients. Can be safely displayed to a client... * * @package GraphQL\Error */ @@ -17,4 +17,12 @@ class UserError extends \RuntimeException implements ClientAware { return true; } + + /** + * @return string + */ + public function getCategory() + { + return 'user'; + } } diff --git a/src/Server/Helper.php b/src/Server/Helper.php index de15317..610b26e 100644 --- a/src/Server/Helper.php +++ b/src/Server/Helper.php @@ -98,7 +98,7 @@ class Helper $operationType = AST::getOperation($doc, $op->operation); if ($op->isReadOnly() && $operationType !== 'query') { - throw new Error("GET supports only query operation"); + throw new RequestError("GET supports only query operation"); } $validationErrors = DocumentValidator::validate( @@ -123,6 +123,10 @@ class Helper $config->getDefaultFieldResolver() ); } + } catch (RequestError $e) { + $result = $promiseAdapter->createFulfilled( + new ExecutionResult(null, [Error::createLocatedError($e)]) + ); } catch (Error $e) { $result = $promiseAdapter->createFulfilled( new ExecutionResult(null, [$e]) @@ -159,7 +163,7 @@ class Helper $loader = $config->getPersistentQueryLoader(); if (!$loader) { - throw new Error("Persisted queries are not supported by this server"); + throw new RequestError("Persisted queries are not supported by this server"); } $source = $loader($op->queryId, $op); @@ -266,10 +270,10 @@ class Helper $bodyParams = json_decode($rawBody ?: '', true); if (json_last_error()) { - throw new Error("Could not parse JSON: " . json_last_error_msg()); + throw new RequestError("Could not parse JSON: " . json_last_error_msg()); } if (!is_array($bodyParams)) { - throw new Error( + throw new RequestError( "GraphQL Server expects JSON object or array, but got " . Utils::printSafeJson($bodyParams) ); @@ -277,9 +281,9 @@ class Helper } else if (stripos($contentType, 'application/x-www-form-urlencoded') !== false) { $bodyParams = $_POST; } else if (null === $contentType) { - throw new Error('Missing "Content-Type" header'); + throw new RequestError('Missing "Content-Type" header'); } else { - throw new Error("Unexpected content type: " . Utils::printSafeJson($contentType)); + throw new RequestError("Unexpected content type: " . Utils::printSafeJson($contentType)); } } @@ -395,7 +399,7 @@ class Helper $result = OperationParams::create($bodyParams); } } else { - throw new Error('HTTP Method "' . $method . '" is not supported'); + throw new RequestError('HTTP Method "' . $method . '" is not supported'); } return $result; } @@ -418,33 +422,33 @@ class Helper { $errors = []; if (!$params->query && !$params->queryId) { - $errors[] = new Error('GraphQL Request must include at least one of those two parameters: "query" or "queryId"'); + $errors[] = new RequestError('GraphQL Request must include at least one of those two parameters: "query" or "queryId"'); } if ($params->query && $params->queryId) { - $errors[] = new Error('GraphQL Request parameters "query" and "queryId" are mutually exclusive'); + $errors[] = new RequestError('GraphQL Request parameters "query" and "queryId" are mutually exclusive'); } if ($params->query !== null && (!is_string($params->query) || empty($params->query))) { - $errors[] = new Error( + $errors[] = new RequestError( 'GraphQL Request parameter "query" must be string, but got ' . Utils::printSafeJson($params->query) ); } if ($params->queryId !== null && (!is_string($params->queryId) || empty($params->queryId))) { - $errors[] = new Error( + $errors[] = new RequestError( 'GraphQL Request parameter "queryId" must be string, but got ' . Utils::printSafeJson($params->queryId) ); } if ($params->operation !== null && (!is_string($params->operation) || empty($params->operation))) { - $errors[] = new Error( + $errors[] = new RequestError( 'GraphQL Request parameter "operation" must be string, but got ' . Utils::printSafeJson($params->operation) ); } if ($params->variables !== null && (!is_array($params->variables) || isset($params->variables[0]))) { - $errors[] = new Error( + $errors[] = new RequestError( 'GraphQL Request parameter "variables" must be object or JSON string parsed to object, but got ' . Utils::printSafeJson($params->getOriginalInput('variables')) ); diff --git a/src/Server/RequestError.php b/src/Server/RequestError.php new file mode 100644 index 0000000..d07f527 --- /dev/null +++ b/src/Server/RequestError.php @@ -0,0 +1,29 @@ + 'We are testing this error', 'locations' => [['line' => 2, 'column' => 7]], - 'path' => ['pets', 0] + 'path' => ['pets', 0], ], [ 'message' => 'We are testing this error', @@ -196,7 +196,7 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase ] ]; - $this->assertEquals($expected, $result); + $this->assertArraySubset($expected, $result); } /** @@ -380,7 +380,7 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase ] ]; - $this->assertEquals($expected, $result); + $this->assertArraySubset($expected, $result); } /** @@ -481,7 +481,7 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase ] ]; - $this->assertEquals($expected, $result); + $this->assertArraySubset($expected, $result); } /** @@ -655,6 +655,6 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase ] ]; - $this->assertEquals($expected, $result); + $this->assertArraySubset($expected, $result); } } diff --git a/tests/Executor/AbstractTest.php b/tests/Executor/AbstractTest.php index 1bb2ecb..024a5bf 100644 --- a/tests/Executor/AbstractTest.php +++ b/tests/Executor/AbstractTest.php @@ -274,7 +274,7 @@ class AbstractTest extends \PHPUnit_Framework_TestCase ]; $actual = GraphQL::execute($schema, $query); - $this->assertEquals($expected, $actual); + $this->assertArraySubset($expected, $actual); } /** @@ -369,7 +369,7 @@ class AbstractTest extends \PHPUnit_Framework_TestCase 'path' => ['pets', 2] ]] ]; - $this->assertEquals($expected, $result); + $this->assertArraySubset($expected, $result); } /** diff --git a/tests/Executor/ExecutorTest.php b/tests/Executor/ExecutorTest.php index 24120de..5e62935 100644 --- a/tests/Executor/ExecutorTest.php +++ b/tests/Executor/ExecutorTest.php @@ -522,7 +522,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase $result = Executor::execute($schema, $docAst, $data)->toArray(); - $this->assertEquals($expected, $result); + $this->assertArraySubset($expected, $result); } /** @@ -615,7 +615,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ]; - $this->assertEquals($expected, $result->toArray()); + $this->assertArraySubset($expected, $result->toArray()); } /** @@ -645,7 +645,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ]; - $this->assertEquals($expected, $result->toArray()); + $this->assertArraySubset($expected, $result->toArray()); } /** @@ -683,7 +683,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ]; - $this->assertEquals($expected, $result->toArray()); + $this->assertArraySubset($expected, $result->toArray()); } /** @@ -996,7 +996,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ]; - $this->assertEquals($expected, $result->toArray()); + $this->assertArraySubset($expected, $result->toArray()); } /** diff --git a/tests/Executor/VariablesTest.php b/tests/Executor/VariablesTest.php index a8107f9..aba2f8f 100644 --- a/tests/Executor/VariablesTest.php +++ b/tests/Executor/VariablesTest.php @@ -162,7 +162,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'message' => 'Variable "$input" got invalid value {"a":"foo","b":"bar","c":null}.'. "\n". 'In field "c": Expected "String!", found null.', - 'locations' => [['line' => 2, 'column' => 17]] + 'locations' => [['line' => 2, 'column' => 17]], + 'category' => 'graphql' ] ] ]; @@ -178,6 +179,7 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'Variable "$input" got invalid value "foo bar".' . "\n" . 'Expected "TestInputObject", found not an object.', 'locations' => [ [ 'line' => 2, 'column' => 17 ] ], + 'category' => 'graphql', ] ] ]; @@ -193,7 +195,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'message' => 'Variable "$input" got invalid value {"a":"foo","b":"bar"}.'. "\n". 'In field "c": Expected "String!", found null.', - 'locations' => [['line' => 2, 'column' => 17]] + 'locations' => [['line' => 2, 'column' => 17]], + 'category' => 'graphql', ] ] ]; @@ -216,7 +219,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'Variable "$input" got invalid value {"na":{"a":"foo"}}.' . "\n" . 'In field "na": In field "c": Expected "String!", found null.' . "\n" . 'In field "nb": Expected "String!", found null.', - 'locations' => [['line' => 2, 'column' => 19]] + 'locations' => [['line' => 2, 'column' => 19]], + 'category' => 'graphql', ] ] ]; @@ -232,7 +236,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'message' => 'Variable "$input" got invalid value {"a":"foo","b":"bar","c":"baz","d":"dog"}.'."\n". 'In field "d": Expected type "ComplexScalar", found "dog".', - 'locations' => [['line' => 2, 'column' => 17]] + 'locations' => [['line' => 2, 'column' => 17]], + 'category' => 'graphql', ] ] ]; @@ -374,7 +379,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'errors' => [ [ 'message' => 'Variable "$value" of required type "String!" was not provided.', - 'locations' => [['line' => 2, 'column' => 31]] + 'locations' => [['line' => 2, 'column' => 31]], + 'category' => 'graphql' ] ] ]; @@ -399,7 +405,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'message' => 'Variable "$value" got invalid value null.' . "\n". 'Expected "String!", found null.', - 'locations' => [['line' => 2, 'column' => 31]] + 'locations' => [['line' => 2, 'column' => 31]], + 'category' => 'graphql', ] ] ]; @@ -453,7 +460,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'errors' => [[ 'message' => 'Argument "input" of required type "String!" was not provided.', 'locations' => [ [ 'line' => 3, 'column' => 9 ] ], - 'path' => [ 'fieldWithNonNullableStringInput' ] + 'path' => [ 'fieldWithNonNullableStringInput' ], + 'category' => 'graphql', ]] ]; $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); @@ -483,7 +491,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'Argument "input" of required type "String!" was provided the ' . 'variable "$foo" which was not provided a runtime value.', 'locations' => [['line' => 3, 'column' => 48]], - 'path' => ['fieldWithNonNullableStringInput'] + 'path' => ['fieldWithNonNullableStringInput'], + 'category' => 'graphql', ]] ]; $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); @@ -555,7 +564,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'message' => 'Variable "$input" got invalid value null.' . "\n" . 'Expected "[String]!", found null.', - 'locations' => [['line' => 2, 'column' => 17]] + 'locations' => [['line' => 2, 'column' => 17]], + 'category' => 'graphql', ] ] ]; @@ -642,7 +652,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'message' => 'Variable "$input" got invalid value ["A",null,"B"].' . "\n" . 'In element #1: Expected "String!", found null.', - 'locations' => [ ['line' => 2, 'column' => 17] ] + 'locations' => [ ['line' => 2, 'column' => 17] ], + 'category' => 'graphql', ] ] ]; @@ -667,7 +678,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'message' => 'Variable "$input" got invalid value null.' . "\n" . 'Expected "[String!]!", found null.', - 'locations' => [ ['line' => 2, 'column' => 17] ] + 'locations' => [ ['line' => 2, 'column' => 17] ], + 'category' => 'graphql', ] ] ]; @@ -707,7 +719,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'message' => 'Variable "$input" got invalid value ["A",null,"B"].'."\n". 'In element #1: Expected "String!", found null.', - 'locations' => [ ['line' => 2, 'column' => 17] ] + 'locations' => [ ['line' => 2, 'column' => 17] ], + 'category' => 'graphql', ] ] ]; @@ -733,7 +746,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'message' => 'Variable "$input" expected value of type "TestType!" which cannot ' . 'be used as an input type.', - 'locations' => [['line' => 2, 'column' => 25]] + 'locations' => [['line' => 2, 'column' => 25]], + 'category' => 'graphql', ] ] ]; @@ -760,7 +774,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'message' => 'Variable "$input" expected value of type "UnknownType!" which ' . 'cannot be used as an input type.', - 'locations' => [['line' => 2, 'column' => 25]] + 'locations' => [['line' => 2, 'column' => 25]], + 'category' => 'graphql', ] ] ]; @@ -814,7 +829,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'Argument "input" got invalid value WRONG_TYPE.' . "\n" . 'Expected type "String", found WRONG_TYPE.', 'locations' => [ [ 'line' => 2, 'column' => 50 ] ], - 'path' => [ 'fieldWithDefaultArgumentValue' ] + 'path' => [ 'fieldWithDefaultArgumentValue' ], + 'category' => 'graphql', ]] ]; diff --git a/tests/Server/QueryExecutionTest.php b/tests/Server/QueryExecutionTest.php index 81f1a85..cee3bae 100644 --- a/tests/Server/QueryExecutionTest.php +++ b/tests/Server/QueryExecutionTest.php @@ -277,7 +277,8 @@ class QueryExecutionTest extends \PHPUnit_Framework_TestCase $expected = [ 'errors' => [ [ - 'message' => 'Persisted queries are not supported by this server' + 'message' => 'Persisted queries are not supported by this server', + 'category' => 'request' ] ] ]; @@ -291,7 +292,8 @@ class QueryExecutionTest extends \PHPUnit_Framework_TestCase $expected = [ 'errors' => [ [ - 'message' => 'GET supports only query operation' + 'message' => 'GET supports only query operation', + 'category' => 'request' ] ] ]; @@ -354,7 +356,8 @@ class QueryExecutionTest extends \PHPUnit_Framework_TestCase 'errors' => [ [ 'message' => 'Cannot query field "invalid" on type "Query".', - 'locations' => [ ['line' => 1, 'column' => 2] ] + 'locations' => [ ['line' => 1, 'column' => 2] ], + 'category' => 'graphql' ] ] ]; @@ -391,7 +394,8 @@ class QueryExecutionTest extends \PHPUnit_Framework_TestCase 'errors' => [ [ 'message' => 'Cannot query field "invalid2" on type "Query".', - 'locations' => [ ['line' => 1, 'column' => 2] ] + 'locations' => [ ['line' => 1, 'column' => 2] ], + 'category' => 'graphql' ] ] ]; diff --git a/tests/Server/RequestParsingTest.php b/tests/Server/RequestParsingTest.php index 5f14cc2..8a100f4 100644 --- a/tests/Server/RequestParsingTest.php +++ b/tests/Server/RequestParsingTest.php @@ -5,6 +5,7 @@ use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; use GraphQL\Server\Helper; use GraphQL\Server\OperationParams; +use GraphQL\Server\RequestError; class RequestParsingTest extends \PHPUnit_Framework_TestCase { @@ -126,7 +127,7 @@ class RequestParsingTest extends \PHPUnit_Framework_TestCase { $body = 'not really{} a json'; - $this->setExpectedException(Error::class, 'Could not parse JSON: Syntax error'); + $this->setExpectedException(RequestError::class, 'Could not parse JSON: Syntax error'); $this->parseRawRequest('application/json', $body); } @@ -134,25 +135,25 @@ class RequestParsingTest extends \PHPUnit_Framework_TestCase { $body = '"str"'; - $this->setExpectedException(Error::class, 'GraphQL Server expects JSON object or array, but got "str"'); + $this->setExpectedException(RequestError::class, 'GraphQL Server expects JSON object or array, but got "str"'); $this->parseRawRequest('application/json', $body); } public function testFailsParsingInvalidContentType() { - $this->setExpectedException(Error::class, 'Unexpected content type: "not-supported-content-type"'); + $this->setExpectedException(RequestError::class, 'Unexpected content type: "not-supported-content-type"'); $this->parseRawRequest('not-supported-content-type', 'test'); } public function testFailsWithMissingContentType() { - $this->setExpectedException(Error::class, 'Missing "Content-Type" header'); + $this->setExpectedException(RequestError::class, 'Missing "Content-Type" header'); $this->parseRawRequest(null, 'test'); } public function testFailsOnMethodsOtherThanPostOrGet() { - $this->setExpectedException(Error::class, 'HTTP Method "PUT" is not supported'); + $this->setExpectedException(RequestError::class, 'HTTP Method "PUT" is not supported'); $this->parseRawRequest(null, 'test', "PUT"); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 0ae8eb0..984c858 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -487,10 +487,10 @@ class ServerTest extends \PHPUnit_Framework_TestCase 'locations' => [[ 'line' => 1, 'column' => 2 - ]] + ]], ]] ]; - $this->assertEquals($expected, $result->toArray()); + $this->assertArraySubset($expected, $result->toArray()); $server->setDebug(Server::DEBUG_EXCEPTIONS); $server->setExceptionFormatter(function($e) { @@ -507,7 +507,7 @@ class ServerTest extends \PHPUnit_Framework_TestCase $result = $server->executeQuery('{withException}'); $expected['errors'][0]['exception'] = ['test' => 'Error']; - $this->assertEquals($expected, $result->toArray()); + $this->assertArraySubset($expected, $result->toArray()); } public function testHandleRequest() @@ -530,7 +530,7 @@ class ServerTest extends \PHPUnit_Framework_TestCase $mock->handleRequest(); $this->assertInternalType('array', $output); - $this->assertEquals(['errors' => [['message' => 'Unexpected Error']]], $output[0]); + $this->assertArraySubset(['errors' => [['message' => 'Unexpected Error']]], $output[0]); $this->assertEquals(500, $output[1]); $output = null; @@ -565,7 +565,8 @@ class ServerTest extends \PHPUnit_Framework_TestCase 'locations' => [[ 'line' => 1, 'column' => 2 - ]] + ]], + 'category' => 'graphql', ]]], 200 ]; diff --git a/tests/Type/IntrospectionTest.php b/tests/Type/IntrospectionTest.php index 8d3f4b9..fcab04d 100644 --- a/tests/Type/IntrospectionTest.php +++ b/tests/Type/IntrospectionTest.php @@ -1481,7 +1481,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase ) ] ]; - $this->assertEquals($expected, GraphQL::execute($schema, $request)); + $this->assertArraySubset($expected, GraphQL::execute($schema, $request)); } /**