Default error reporting now includes "category" key for every error

This commit is contained in:
Vladimir Razuvaev 2017-08-08 02:00:17 +07:00
parent 09070485c1
commit f911fac7b1
14 changed files with 164 additions and 65 deletions

View File

@ -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();
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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';
}
}

View File

@ -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'))
);

View File

@ -0,0 +1,29 @@
<?php
namespace GraphQL\Server;
use GraphQL\Error\ClientAware;
class RequestError extends \Exception implements ClientAware
{
/**
* Returns true when exception message is safe to be displayed to client
*
* @return bool
*/
public function isClientSafe()
{
return true;
}
/**
* Returns string describing error category. E.g. "validation" for your own validation errors.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @return string
*/
public function getCategory()
{
return 'request';
}
}

View File

@ -186,7 +186,7 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase
[
'message' => '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);
}
}

View File

@ -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);
}
/**

View File

@ -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());
}
/**

View File

@ -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',
]]
];

View File

@ -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'
]
]
];

View File

@ -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");
}

View File

@ -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
];

View File

@ -1481,7 +1481,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
)
]
];
$this->assertEquals($expected, GraphQL::execute($schema, $request));
$this->assertArraySubset($expected, GraphQL::execute($schema, $request));
}
/**