Default error formatter now returns "Internal server error" unless error is client-aware and safe to report directly to end-users

This commit is contained in:
Vladimir Razuvaev 2017-07-18 20:57:30 +07:00
parent fbcd20814a
commit 38922dbbed
17 changed files with 357 additions and 156 deletions

View File

@ -1,15 +1,43 @@
# Upgrade # Upgrade
## Upgrade v0.9.x > v0.10.x ## Upgrade v0.9.x > v0.10.x
### Deprecated: GraphQL\Utils moved to GraphQL\Utils\Utils #### Breaking: default error formatting
Old class still exists, but triggers deprecation warning. By default exceptions thrown in resolvers will be reported with generic message `"Internal server error"`.
Only exceptions implementing interface `GraphQL\Error\ClientAware` and claiming themselves as `safe` will
be reported with full error message.
This is done to avoid information leak in production when unhandled exceptions occur in resolvers
(e.g. database connection errors, file access errors, etc).
During development or debugging use `$executionResult->toArray(true)`. It will add `debugMessage` key to
each error entry in result. If you also want to add `trace` for each error - pass flags instead:
```
use GraphQL\Error\FormattedError;
$debug = FormattedError::INCLUDE_DEBUG_MESSAGE | FormattedError::INCLUDE_TRACE;
$result = GraphQL::executeAndReturnResult()->toArray($debug);
```
To change default `"Internal server error"` message to something else, use:
```
GraphQL\Error\FormattedError::setInternalErrorMessage("Unexpected error");
```
**This change only affects default error reporting mechanism. If you set your own error formatter using
`ExecutionResult::setErrorFormatter()` you won't be affected by this change.**
If you are not happy with this change just set [your own error
formatter](http://webonyx.github.io/graphql-php/error-handling/#custom-error-formatting).
#### Deprecated: GraphQL\Utils moved to GraphQL\Utils\Utils
Old class still exists, but triggers deprecation warning when referenced.
## Upgrade v0.7.x > v0.8.x ## Upgrade v0.7.x > v0.8.x
All of those changes apply to those who extends various parts of this library. All of those changes apply to those who extends various parts of this library.
If you only use the library and don't try to extend it - everything should work without breaks. If you only use the library and don't try to extend it - everything should work without breaks.
### Breaking: Custom directives handling #### Breaking: Custom directives handling
When passing custom directives to schema, default directives (like `@skip` and `@include`) When passing custom directives to schema, default directives (like `@skip` and `@include`)
are not added to schema automatically anymore. If you need them - add them explicitly with are not added to schema automatically anymore. If you need them - add them explicitly with
your other directives. your other directives.
@ -30,15 +58,15 @@ $schema = new Schema([
]); ]);
``` ```
### Breaking: Schema protected property and methods visibility #### Breaking: Schema protected property and methods visibility
Most of the `protected` properties and methods of `GraphQL\Schema` were changed to `private`. Most of the `protected` properties and methods of `GraphQL\Schema` were changed to `private`.
Please use public interface instead. Please use public interface instead.
### Breaking: Node kind constants #### Breaking: Node kind constants
Node kind constants were extracted from `GraphQL\Language\AST\Node` to Node kind constants were extracted from `GraphQL\Language\AST\Node` to
separate class `GraphQL\Language\AST\NodeKind` separate class `GraphQL\Language\AST\NodeKind`
### Non-breaking: AST node classes renamed #### Non-breaking: AST node classes renamed
AST node classes were renamed to disambiguate with types. e.g.: AST node classes were renamed to disambiguate with types. e.g.:
``` ```
@ -50,7 +78,7 @@ etc.
Old names are still available via `class_alias` defined in `src/deprecated.php`. Old names are still available via `class_alias` defined in `src/deprecated.php`.
This file is included automatically when using composer autoloading. This file is included automatically when using composer autoloading.
### Deprecations #### Deprecations
There are several deprecations which still work, but trigger `E_USER_DEPRECATED` when used. There are several deprecations which still work, but trigger `E_USER_DEPRECATED` when used.
For example `GraphQL\Executor\Executor::setDefaultResolveFn()` is renamed to `setDefaultResolver()` For example `GraphQL\Executor\Executor::setDefaultResolveFn()` is renamed to `setDefaultResolver()`
@ -61,7 +89,7 @@ but still works with old name.
There are a few new breaking changes in v0.7.0 that were added to the graphql-js reference implementation There are a few new breaking changes in v0.7.0 that were added to the graphql-js reference implementation
with the spec of April2016 with the spec of April2016
### 1. Context for resolver #### 1. Context for resolver
You can now pass a custom context to the `GraphQL::execute` function that is available in all resolvers as 3rd argument. You can now pass a custom context to the `GraphQL::execute` function that is available in all resolvers as 3rd argument.
This can for example be used to pass the current user etc. This can for example be used to pass the current user etc.
@ -119,7 +147,7 @@ function resolveMyField($object, array $args, $context, ResolveInfo $info){
} }
``` ```
### 2. Schema constructor signature #### 2. Schema constructor signature
The signature of the Schema constructor now accepts an associative config array instead of positional arguments: The signature of the Schema constructor now accepts an associative config array instead of positional arguments:
@ -137,7 +165,7 @@ $schema = new Schema([
]); ]);
``` ```
### 3. Types can be directly passed to schema #### 3. Types can be directly passed to schema
There are edge cases when GraphQL cannot infer some types from your schema. There are edge cases when GraphQL cannot infer some types from your schema.
One example is when you define a field of interface type and object types implementing One example is when you define a field of interface type and object types implementing

View File

@ -113,6 +113,28 @@ exceptions thrown by resolvers. To access original exceptions use `$error->getPr
But note that previous exception is only available for **Execution** errors and will be `null` But note that previous exception is only available for **Execution** errors and will be `null`
for **Syntax** or **Validation** errors. for **Syntax** or **Validation** errors.
For example:
```php
$result = GraphQL::executeAndReturnResult()
->setErrorFormatter(function(GraphQL\Error\Error $err) {
$resolverException = $err->getPrevious();
if ($resolverException instanceof MyResolverException) {
$formattedError = [
'message' => $resolverException->getMessage(),
'code' => $resolverException->getCode()
];
} else {
$formattedError = [
'message' => $err->getMessage()
];
}
return $formattedError;
})
->toArray();
```
# Schema Errors # Schema Errors
So far we only covered errors which occur during query execution process. But schema definition can So far we only covered errors which occur during query execution process. But schema definition can
also throw if there is an error in one of type definitions. also throw if there is an error in one of type definitions.

17
src/Error/ClientAware.php Normal file
View File

@ -0,0 +1,17 @@
<?php
namespace GraphQL\Error;
/**
* Interface ClientAware
*
* @package GraphQL\Error
*/
interface ClientAware
{
/**
* Returns true when exception message is safe to be displayed to client
*
* @return bool
*/
public function isClientSafe();
}

View File

@ -14,7 +14,7 @@ use GraphQL\Utils\Utils;
* *
* @package GraphQL * @package GraphQL
*/ */
class Error extends \Exception implements \JsonSerializable class Error extends \Exception implements \JsonSerializable, ClientAware
{ {
/** /**
* A message describing the Error for debugging purposes. * A message describing the Error for debugging purposes.
@ -62,6 +62,11 @@ class Error extends \Exception implements \JsonSerializable
*/ */
private $positions; private $positions;
/**
* @var bool
*/
private $isClientSafe;
/** /**
* Given an arbitrary Error, presumably thrown while attempting to execute a * Given an arbitrary Error, presumably thrown while attempting to execute a
* GraphQL operation, produce a new GraphQLError aware of the location in the * GraphQL operation, produce a new GraphQLError aware of the location in the
@ -133,6 +138,22 @@ class Error extends \Exception implements \JsonSerializable
$this->source = $source; $this->source = $source;
$this->positions = $positions; $this->positions = $positions;
$this->path = $path; $this->path = $path;
if ($previous instanceof ClientAware) {
$this->isClientSafe = $previous->isClientSafe();
} else if ($previous === null) {
$this->isClientSafe = true;
} else {
$this->isClientSafe = false;
}
}
/**
* @return bool
*/
public function isClientSafe()
{
return $this->isClientSafe;
} }
/** /**
@ -190,6 +211,7 @@ class Error extends \Exception implements \JsonSerializable
/** /**
* Returns array representation of error suitable for serialization * Returns array representation of error suitable for serialization
* *
* @deprecated Use FormattedError::createFromException() instead
* @return array * @return array
*/ */
public function toSerializableArray() public function toSerializableArray()

View File

@ -13,20 +13,67 @@ use GraphQL\Utils\Utils;
*/ */
class FormattedError class FormattedError
{ {
const INCLUDE_DEBUG_MESSAGE = 1;
const INCLUDE_TRACE = 2;
private static $internalErrorMessage = 'Internal server error';
public static function setInternalErrorMessage($msg)
{
self::$internalErrorMessage = $msg;
}
/** /**
* @param \Throwable $e * @param \Throwable $e
* @param $debug * @param bool|int $debug
* @param string $internalErrorMessage
* *
* @return array * @return array
*/ */
public static function createFromException($e, $debug = false) public static function createFromException($e, $debug = false, $internalErrorMessage = null)
{ {
if ($e instanceof Error) { Utils::invariant(
$result = $e->toSerializableArray(); $e instanceof \Exception || $e instanceof \Throwable,
} else if ($e instanceof \ErrorException) { "Expected exception, got %s",
Utils::getVariableType($e)
);
$debug = (int) $debug;
$internalErrorMessage = $internalErrorMessage ?: self::$internalErrorMessage;
if ($e instanceof ClientAware) {
if ($e->isClientSafe()) {
$result = [ $result = [
'message' => $e->getMessage(), 'message' => $e->getMessage()
]; ];
} else {
$result = [
'message' => $internalErrorMessage,
'isInternalError' => true
];
}
} else {
$result = [
'message' => $internalErrorMessage,
'isInternalError' => true
];
}
if (($debug & self::INCLUDE_DEBUG_MESSAGE > 0) && $result['message'] === $internalErrorMessage) {
$result['debugMessage'] = $e->getMessage();
}
if ($e instanceof Error) {
$locations = Utils::map($e->getLocations(), function(SourceLocation $loc) {
return $loc->toSerializableArray();
});
if (!empty($locations)) {
$result['locations'] = $locations;
}
if (!empty($e->path)) {
$result['path'] = $e->path;
}
} else if ($e instanceof \ErrorException) {
if ($debug) { if ($debug) {
$result += [ $result += [
'file' => $e->getFile(), 'file' => $e->getFile(),
@ -34,18 +81,16 @@ class FormattedError
'severity' => $e->getSeverity() 'severity' => $e->getSeverity()
]; ];
} }
} else { } else if ($e instanceof \Error) {
Utils::invariant( if ($debug) {
$e instanceof \Exception || $e instanceof \Throwable, $result += [
"Expected exception, got %s", 'file' => $e->getFile(),
Utils::getVariableType($e) 'line' => $e->getLine(),
);
$result = [
'message' => $e->getMessage()
]; ];
} }
}
if ($debug) { if ($debug & self::INCLUDE_TRACE > 0) {
$debugging = $e->getPrevious() ?: $e; $debugging = $e->getPrevious() ?: $e;
$result['trace'] = static::toSafeTrace($debugging->getTrace()); $result['trace'] = static::toSafeTrace($debugging->getTrace());
} }

View File

@ -4,10 +4,17 @@ namespace GraphQL\Error;
/** /**
* Class UserError * Class UserError
* *
* Error that can be safely displayed to client... * Error caused by actions of GraphQL clients. Can be safely displayed to client...
* *
* @package GraphQL\Error * @package GraphQL\Error
*/ */
class UserError extends InvariantViolation class UserError extends \RuntimeException implements ClientAware
{ {
/**
* @return bool
*/
public function isClientSafe()
{
return true;
}
} }

View File

@ -2,6 +2,7 @@
namespace GraphQL\Executor; namespace GraphQL\Executor;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Error\FormattedError;
class ExecutionResult implements \JsonSerializable class ExecutionResult implements \JsonSerializable
{ {
@ -23,7 +24,7 @@ class ExecutionResult implements \JsonSerializable
/** /**
* @var callable * @var callable
*/ */
private $errorFormatter = ['GraphQL\Error\Error', 'formatError']; private $errorFormatter;
/** /**
* @param array $data * @param array $data
@ -48,14 +49,26 @@ class ExecutionResult implements \JsonSerializable
} }
/** /**
* @param bool|int $debug
* @return array * @return array
*/ */
public function toArray() public function toArray($debug = false)
{ {
$result = []; $result = [];
if (!empty($this->errors)) { if (!empty($this->errors)) {
$result['errors'] = array_map($this->errorFormatter, $this->errors); if ($debug) {
$errorFormatter = function($e) use ($debug) {
return FormattedError::createFromException($e, $debug);
};
} else if (!$this->errorFormatter) {
$errorFormatter = function($e) {
return FormattedError::createFromException($e, false);
};
} else {
$errorFormatter = $this->errorFormatter;
}
$result['errors'] = array_map($errorFormatter, $this->errors);
} }
if (null !== $this->data) { if (null !== $this->data) {

View File

@ -2,6 +2,7 @@
namespace GraphQL\Tests\Executor; namespace GraphQL\Tests\Executor;
use GraphQL\Deferred; use GraphQL\Deferred;
use GraphQL\Error\UserError;
use GraphQL\Error\Warning; use GraphQL\Error\Warning;
use GraphQL\GraphQL; use GraphQL\GraphQL;
use GraphQL\Schema; use GraphQL\Schema;
@ -120,7 +121,7 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase
'interfaces' => [$PetType], 'interfaces' => [$PetType],
'isTypeOf' => function () { 'isTypeOf' => function () {
return new Deferred(function () { return new Deferred(function () {
throw new \Exception('We are testing this error'); throw new UserError('We are testing this error');
}); });
}, },
'fields' => [ 'fields' => [
@ -578,7 +579,7 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase
'name' => 'Pet', 'name' => 'Pet',
'resolveType' => function () { 'resolveType' => function () {
return new Deferred(function () { return new Deferred(function () {
throw new \Exception('We are testing this error'); throw new UserError('We are testing this error');
}); });
}, },
'fields' => [ 'fields' => [

View File

@ -5,6 +5,7 @@ require_once __DIR__ . '/TestClasses.php';
use GraphQL\Deferred; use GraphQL\Deferred;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Error\UserError;
use GraphQL\Executor\Executor; use GraphQL\Executor\Executor;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Schema; use GraphQL\Schema;
@ -371,55 +372,55 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
return 'sync'; return 'sync';
}, },
'syncError' => function () { 'syncError' => function () {
throw new \Exception('Error getting syncError'); throw new UserError('Error getting syncError');
}, },
'syncRawError' => function() { 'syncRawError' => function() {
throw new \Exception('Error getting syncRawError'); throw new UserError('Error getting syncRawError');
}, },
// inherited from JS reference implementation, but make no sense in this PHP impl // inherited from JS reference implementation, but make no sense in this PHP impl
// leaving it just to simplify migrations from newer js versions // leaving it just to simplify migrations from newer js versions
'syncReturnError' => function() { 'syncReturnError' => function() {
return new \Exception('Error getting syncReturnError'); return new UserError('Error getting syncReturnError');
}, },
'syncReturnErrorList' => function () { 'syncReturnErrorList' => function () {
return [ return [
'sync0', 'sync0',
new \Exception('Error getting syncReturnErrorList1'), new UserError('Error getting syncReturnErrorList1'),
'sync2', 'sync2',
new \Exception('Error getting syncReturnErrorList3') new UserError('Error getting syncReturnErrorList3')
]; ];
}, },
'async' => function() { 'async' => function() {
return new Deferred(function() { return 'async'; }); return new Deferred(function() { return 'async'; });
}, },
'asyncReject' => function() { 'asyncReject' => function() {
return new Deferred(function() { throw new \Exception('Error getting asyncReject'); }); return new Deferred(function() { throw new UserError('Error getting asyncReject'); });
}, },
'asyncRawReject' => function () { 'asyncRawReject' => function () {
return new Deferred(function() { return new Deferred(function() {
throw new \Exception('Error getting asyncRawReject'); throw new UserError('Error getting asyncRawReject');
}); });
}, },
'asyncEmptyReject' => function () { 'asyncEmptyReject' => function () {
return new Deferred(function() { return new Deferred(function() {
throw new \Exception(); throw new UserError();
}); });
}, },
'asyncError' => function() { 'asyncError' => function() {
return new Deferred(function() { return new Deferred(function() {
throw new \Exception('Error getting asyncError'); throw new UserError('Error getting asyncError');
}); });
}, },
// inherited from JS reference implementation, but make no sense in this PHP impl // inherited from JS reference implementation, but make no sense in this PHP impl
// leaving it just to simplify migrations from newer js versions // leaving it just to simplify migrations from newer js versions
'asyncRawError' => function() { 'asyncRawError' => function() {
return new Deferred(function() { return new Deferred(function() {
throw new \Exception('Error getting asyncRawError'); throw new UserError('Error getting asyncRawError');
}); });
}, },
'asyncReturnError' => function() { 'asyncReturnError' => function() {
return new Deferred(function() { return new Deferred(function() {
throw new \Exception('Error getting asyncReturnError'); throw new UserError('Error getting asyncReturnError');
}); });
}, },
]; ];

View File

@ -3,6 +3,7 @@
namespace GraphQL\Tests\Executor; namespace GraphQL\Tests\Executor;
use GraphQL\Deferred; use GraphQL\Deferred;
use GraphQL\Error\UserError;
use GraphQL\Executor\Executor; use GraphQL\Executor\Executor;
use GraphQL\Error\FormattedError; use GraphQL\Error\FormattedError;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
@ -72,7 +73,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
$this->checkHandlesNullableLists( $this->checkHandlesNullableLists(
function () { function () {
return new Deferred(function () { return new Deferred(function () {
throw new \Exception('bad'); throw new UserError('bad');
}); });
}, },
[ [
@ -131,7 +132,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
return 1; return 1;
}), }),
new Deferred(function() { new Deferred(function() {
throw new \Exception('bad'); throw new UserError('bad');
}), }),
new Deferred(function() { new Deferred(function() {
return 2; return 2;
@ -174,12 +175,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase
[ [
'data' => [ 'nest' => null ], 'data' => [ 'nest' => null ],
'errors' => [ 'errors' => [
FormattedError::create( [
'Cannot return null for non-nullable field DataType.test.', 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ] 'locations' => [['line' => 1, 'column' => 10]]
)
] ]
] ]
],
true
); );
} }
@ -210,19 +212,20 @@ class ListsTest extends \PHPUnit_Framework_TestCase
[ [
'data' => [ 'nest' => null ], 'data' => [ 'nest' => null ],
'errors' => [ 'errors' => [
FormattedError::create( [
'Cannot return null for non-nullable field DataType.test.', 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ] 'locations' => [['line' => 1, 'column' => 10]]
)
] ]
] ]
],
true
); );
// Rejected // Rejected
$this->checkHandlesNonNullableLists( $this->checkHandlesNonNullableLists(
function () { function () {
return new Deferred(function() { return new Deferred(function() {
throw new \Exception('bad'); throw new UserError('bad');
}); });
}, },
[ [
@ -280,7 +283,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
return 1; return 1;
}), }),
new Deferred(function() { new Deferred(function() {
throw new \Exception('bad'); throw new UserError('bad');
}), }),
new Deferred(function() { new Deferred(function() {
return 2; return 2;
@ -317,12 +320,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase
[ [
'data' => [ 'nest' => [ 'test' => null ] ], 'data' => [ 'nest' => [ 'test' => null ] ],
'errors' => [ 'errors' => [
FormattedError::create( [
'Cannot return null for non-nullable field DataType.test.', 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ] 'locations' => [ ['line' => 1, 'column' => 10] ]
)
] ]
] ]
],
true
); );
// Returns null // Returns null
@ -353,12 +357,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase
[ [
'data' => [ 'nest' => [ 'test' => null ] ], 'data' => [ 'nest' => [ 'test' => null ] ],
'errors' => [ 'errors' => [
FormattedError::create( [
'Cannot return null for non-nullable field DataType.test.', 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ] 'locations' => [['line' => 1, 'column' => 10]]
)
] ]
] ]
],
true
); );
// Returns null // Returns null
@ -373,7 +378,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
$this->checkHandlesListOfNonNulls( $this->checkHandlesListOfNonNulls(
function () { function () {
return new Deferred(function() { return new Deferred(function() {
throw new \Exception('bad'); throw new UserError('bad');
}); });
}, },
[ [
@ -425,7 +430,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
return 1; return 1;
}), }),
new Deferred(function() { new Deferred(function() {
throw new \Exception('bad'); throw new UserError('bad');
}), }),
new Deferred(function() { new Deferred(function() {
return 2; return 2;
@ -463,12 +468,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase
[ [
'data' => [ 'nest' => null ], 'data' => [ 'nest' => null ],
'errors' => [ 'errors' => [
FormattedError::create( [
'Cannot return null for non-nullable field DataType.test.', 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ] 'locations' => [['line' => 1, 'column' => 10 ]]
)
] ]
] ]
],
true
); );
// Returns null // Returns null
@ -477,12 +483,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase
[ [
'data' => [ 'nest' => null ], 'data' => [ 'nest' => null ],
'errors' => [ 'errors' => [
FormattedError::create( [
'Cannot return null for non-nullable field DataType.test.', 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ] 'locations' => [ ['line' => 1, 'column' => 10] ]
)
] ]
] ]
],
true
); );
} }
@ -507,12 +514,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase
[ [
'data' => [ 'nest' => null ], 'data' => [ 'nest' => null ],
'errors' => [ 'errors' => [
FormattedError::create( [
'Cannot return null for non-nullable field DataType.test.', 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ] 'locations' => [ ['line' => 1, 'column' => 10] ]
)
] ]
] ]
],
true
); );
// Returns null // Returns null
@ -523,19 +531,20 @@ class ListsTest extends \PHPUnit_Framework_TestCase
[ [
'data' => [ 'nest' => null ], 'data' => [ 'nest' => null ],
'errors' => [ 'errors' => [
FormattedError::create( [
'Cannot return null for non-nullable field DataType.test.', 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ] 'locations' => [ ['line' => 1, 'column' => 10] ]
)
] ]
] ]
],
true
); );
// Rejected // Rejected
$this->checkHandlesNonNullListOfNonNulls( $this->checkHandlesNonNullListOfNonNulls(
function () { function () {
return new Deferred(function() { return new Deferred(function() {
throw new \Exception('bad'); throw new UserError('bad');
}); });
}, },
[ [
@ -586,12 +595,13 @@ class ListsTest extends \PHPUnit_Framework_TestCase
[ [
'data' => [ 'nest' => null ], 'data' => [ 'nest' => null ],
'errors' => [ 'errors' => [
FormattedError::create( [
'Cannot return null for non-nullable field DataType.test.', 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.',
[ new SourceLocation(1, 10) ] 'locations' => [['line' => 1, 'column' => 10]]
)
] ]
] ]
],
true
); );
// Contains reject // Contains reject
@ -602,7 +612,7 @@ class ListsTest extends \PHPUnit_Framework_TestCase
return 1; return 1;
}), }),
new Deferred(function() { new Deferred(function() {
throw new \Exception('bad'); throw new UserError('bad');
}), }),
new Deferred(function() { new Deferred(function() {
return 2; return 2;
@ -628,25 +638,25 @@ class ListsTest extends \PHPUnit_Framework_TestCase
$this->check($testType, $testData, $expected); $this->check($testType, $testData, $expected);
} }
private function checkHandlesNonNullableLists($testData, $expected) private function checkHandlesNonNullableLists($testData, $expected, $debug = false)
{ {
$testType = Type::nonNull(Type::listOf(Type::int())); $testType = Type::nonNull(Type::listOf(Type::int()));
$this->check($testType, $testData, $expected); $this->check($testType, $testData, $expected, $debug);
} }
private function checkHandlesListOfNonNulls($testData, $expected) private function checkHandlesListOfNonNulls($testData, $expected, $debug = false)
{ {
$testType = Type::listOf(Type::nonNull(Type::int())); $testType = Type::listOf(Type::nonNull(Type::int()));
$this->check($testType, $testData, $expected); $this->check($testType, $testData, $expected, $debug);
} }
public function checkHandlesNonNullListOfNonNulls($testData, $expected) public function checkHandlesNonNullListOfNonNulls($testData, $expected, $debug = false)
{ {
$testType = Type::nonNull(Type::listOf(Type::nonNull(Type::int()))); $testType = Type::nonNull(Type::listOf(Type::nonNull(Type::int())));
$this->check($testType, $testData, $expected); $this->check($testType, $testData, $expected, $debug);
} }
private function check($testType, $testData, $expected) private function check($testType, $testData, $expected, $debug = false)
{ {
$data = ['test' => $testData]; $data = ['test' => $testData];
$dataType = null; $dataType = null;
@ -675,6 +685,6 @@ class ListsTest extends \PHPUnit_Framework_TestCase
$ast = Parser::parse('{ nest { test } }'); $ast = Parser::parse('{ nest { test } }');
$result = Executor::execute($schema, $ast, $data); $result = Executor::execute($schema, $ast, $data);
$this->assertArraySubset($expected, $result->toArray()); $this->assertArraySubset($expected, $result->toArray($debug));
} }
} }

View File

@ -106,17 +106,17 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
'sixth' => null, 'sixth' => null,
], ],
'errors' => [ 'errors' => [
FormattedError::create( [
'Cannot change the number', 'debugMessage' => 'Cannot change the number',
[new SourceLocation(8, 7)] 'locations' => [['line' => 8, 'column' => 7]]
), ],
FormattedError::create( [
'Cannot change the number', 'debugMessage' => 'Cannot change the number',
[new SourceLocation(17, 7)] 'locations' => [['line' => 17, 'column' => 7]]
) ]
] ]
]; ];
$this->assertArraySubset($expected, $mutationResult->toArray()); $this->assertArraySubset($expected, $mutationResult->toArray(true));
} }
private function schema() private function schema()

View File

@ -3,6 +3,7 @@
namespace GraphQL\Tests\Executor; namespace GraphQL\Tests\Executor;
use GraphQL\Deferred; use GraphQL\Deferred;
use GraphQL\Error\UserError;
use GraphQL\Executor\Executor; use GraphQL\Executor\Executor;
use GraphQL\Error\FormattedError; use GraphQL\Error\FormattedError;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
@ -31,10 +32,10 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
public function setUp() public function setUp()
{ {
$this->syncError = new \Exception('sync'); $this->syncError = new UserError('sync');
$this->nonNullSyncError = new \Exception('nonNullSync'); $this->nonNullSyncError = new UserError('nonNullSync');
$this->promiseError = new \Exception('promise'); $this->promiseError = new UserError('promise');
$this->nonNullPromiseError = new \Exception('nonNullPromise'); $this->nonNullPromiseError = new UserError('nonNullPromise');
$this->throwingData = [ $this->throwingData = [
'sync' => function () { 'sync' => function () {
@ -482,10 +483,13 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'nest' => null 'nest' => null
], ],
'errors' => [ 'errors' => [
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullSync.', [new SourceLocation(4, 11)]) [
'debugMessage' => 'Cannot return null for non-nullable field DataType.nonNullSync.',
'locations' => [['line' => 4, 'column' => 11]]
]
] ]
]; ];
$this->assertArraySubset($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray()); $this->assertArraySubset($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(true));
} }
public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFieldThatReturnsNullInAPromise() public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFieldThatReturnsNullInAPromise()
@ -505,11 +509,17 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'nest' => null, 'nest' => null,
], ],
'errors' => [ 'errors' => [
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullPromise.', [new SourceLocation(4, 11)]), [
'debugMessage' => 'Cannot return null for non-nullable field DataType.nonNullPromise.',
'locations' => [['line' => 4, 'column' => 11]]
],
] ]
]; ];
$this->assertArraySubset($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray()); $this->assertArraySubset(
$expected,
Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(true)
);
} }
public function testNullsAnObjectReturnedInAPromiseThatContainsANonNullableFieldThatReturnsNullSynchronously() public function testNullsAnObjectReturnedInAPromiseThatContainsANonNullableFieldThatReturnsNullSynchronously()
@ -529,11 +539,17 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'promiseNest' => null, 'promiseNest' => null,
], ],
'errors' => [ 'errors' => [
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullSync.', [new SourceLocation(4, 11)]), [
'debugMessage' => 'Cannot return null for non-nullable field DataType.nonNullSync.',
'locations' => [['line' => 4, 'column' => 11]]
],
] ]
]; ];
$this->assertArraySubset($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray()); $this->assertArraySubset(
$expected,
Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(true)
);
} }
public function testNullsAnObjectReturnedInAPromiseThatContainsANonNullableFieldThatReturnsNullInaAPromise() public function testNullsAnObjectReturnedInAPromiseThatContainsANonNullableFieldThatReturnsNullInaAPromise()
@ -553,11 +569,17 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'promiseNest' => null, 'promiseNest' => null,
], ],
'errors' => [ 'errors' => [
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullPromise.', [new SourceLocation(4, 11)]), [
'debugMessage' => 'Cannot return null for non-nullable field DataType.nonNullPromise.',
'locations' => [['line' => 4, 'column' => 11]]
],
] ]
]; ];
$this->assertArraySubset($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray()); $this->assertArraySubset(
$expected,
Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(true)
);
} }
public function testNullsAComplexTreeOfNullableFieldsThatReturnNull() public function testNullsAComplexTreeOfNullableFieldsThatReturnNull()
@ -687,14 +709,17 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
'anotherPromiseNest' => null, 'anotherPromiseNest' => null,
], ],
'errors' => [ 'errors' => [
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullSync.', [new SourceLocation(8, 19)]), ['debugMessage' => 'Cannot return null for non-nullable field DataType.nonNullSync.', 'locations' => [ ['line' => 8, 'column' => 19]]],
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullSync.', [new SourceLocation(19, 19)]), ['debugMessage' => 'Cannot return null for non-nullable field DataType.nonNullSync.', 'locations' => [ ['line' => 19, 'column' => 19]]],
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullPromise.', [new SourceLocation(30, 19)]), ['debugMessage' => 'Cannot return null for non-nullable field DataType.nonNullPromise.', 'locations' => [ ['line' => 30, 'column' => 19]]],
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullPromise.', [new SourceLocation(41, 19)]), ['debugMessage' => 'Cannot return null for non-nullable field DataType.nonNullPromise.', 'locations' => [ ['line' => 41, 'column' => 19]]],
] ]
]; ];
$this->assertArraySubset($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray()); $this->assertArraySubset(
$expected,
Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(true)
);
} }
/** /**
@ -743,10 +768,16 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
$expected = [ $expected = [
'errors' => [ 'errors' => [
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullSync.', [new SourceLocation(2, 17)]), [
'debugMessage' => 'Cannot return null for non-nullable field DataType.nonNullSync.',
'locations' => [['line' => 2, 'column' => 17]]
],
] ]
]; ];
$this->assertArraySubset($expected, Executor::execute($this->schema, Parser::parse($doc), $this->nullingData)->toArray()); $this->assertArraySubset(
$expected,
Executor::execute($this->schema, Parser::parse($doc), $this->nullingData)->toArray(true)
);
} }
public function testNullsTheTopLevelIfAsyncNonNullableFieldResolvesNull() public function testNullsTheTopLevelIfAsyncNonNullableFieldResolvesNull()
@ -760,10 +791,16 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
$expected = [ $expected = [
'data' => null, 'data' => null,
'errors' => [ 'errors' => [
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullPromise.', [new SourceLocation(2, 17)]), [
'debugMessage' => 'Cannot return null for non-nullable field DataType.nonNullPromise.',
'locations' => [['line' => 2, 'column' => 17]]
],
] ]
]; ];
$this->assertArraySubset($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray()); $this->assertArraySubset(
$expected,
Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(true)
);
} }
} }

View File

@ -48,7 +48,7 @@ class QueryExecutionTest extends \PHPUnit_Framework_TestCase
'fieldWithException' => [ 'fieldWithException' => [
'type' => Type::string(), 'type' => Type::string(),
'resolve' => function($root, $args, $context, $info) { 'resolve' => function($root, $args, $context, $info) {
throw new \Exception("This is the exception we want"); throw new UserError("This is the exception we want");
} }
], ],
'testContextAndRootValue' => [ 'testContextAndRootValue' => [
@ -122,10 +122,10 @@ class QueryExecutionTest extends \PHPUnit_Framework_TestCase
], ],
'extensions' => [ 'extensions' => [
'phpErrors' => [ 'phpErrors' => [
['message' => 'deprecated', 'severity' => 16384], ['debugMessage' => 'deprecated', 'severity' => 16384],
['message' => 'notice', 'severity' => 1024], ['debugMessage' => 'notice', 'severity' => 1024],
['message' => 'warning', 'severity' => 512], ['debugMessage' => 'warning', 'severity' => 512],
['message' => 'Undefined index: test', 'severity' => 8], ['debugMessage' => 'Undefined index: test', 'severity' => 8],
] ]
] ]
]; ];
@ -518,7 +518,7 @@ class QueryExecutionTest extends \PHPUnit_Framework_TestCase
private function assertQueryResultEquals($expected, $query, $variables = null) private function assertQueryResultEquals($expected, $query, $variables = null)
{ {
$result = $this->executeQuery($query, $variables); $result = $this->executeQuery($query, $variables);
$this->assertArraySubset($expected, $result->toArray()); $this->assertArraySubset($expected, $result->toArray(true));
return $result; return $result;
} }
} }

View File

@ -6,9 +6,6 @@ use GraphQL\Error\UserError;
use GraphQL\Server\Helper; use GraphQL\Server\Helper;
use GraphQL\Server\OperationParams; use GraphQL\Server\OperationParams;
/**
* @backupGlobals enabled
*/
class RequestParsingTest extends \PHPUnit_Framework_TestCase class RequestParsingTest extends \PHPUnit_Framework_TestCase
{ {
public function testParsesGraphqlRequest() public function testParsesGraphqlRequest()

View File

@ -3,6 +3,7 @@ namespace GraphQL\Tests;
use GraphQL\Error\FormattedError; use GraphQL\Error\FormattedError;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Error\UserError;
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter; use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Server; use GraphQL\Server;
@ -465,7 +466,7 @@ class ServerTest extends \PHPUnit_Framework_TestCase
'withException' => [ 'withException' => [
'type' => Type::string(), 'type' => Type::string(),
'resolve' => function() { 'resolve' => function() {
throw new \Exception("Error"); throw new UserError("Error");
} }
] ]
] ]

View File

@ -415,12 +415,12 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
*/ */
public function testMayBeInternallyRepresentedWithComplexValues() public function testMayBeInternallyRepresentedWithComplexValues()
{ {
$result = GraphQL::execute($this->schema, '{ $result = GraphQL::executeAndReturnResult($this->schema, '{
first: complexEnum first: complexEnum
second: complexEnum(fromEnum: TWO) second: complexEnum(fromEnum: TWO)
good: complexEnum(provideGoodValue: true) good: complexEnum(provideGoodValue: true)
bad: complexEnum(provideBadValue: true) bad: complexEnum(provideBadValue: true)
}'); }')->toArray(true);
$expected = [ $expected = [
'data' => [ 'data' => [
@ -430,7 +430,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
'bad' => null 'bad' => null
], ],
'errors' => [[ 'errors' => [[
'message' => 'debugMessage' =>
'Expected a value of type "Complex" but received: instance of ArrayObject', 'Expected a value of type "Complex" but received: instance of ArrayObject',
'locations' => [['line' => 5, 'column' => 9]] 'locations' => [['line' => 5, 'column' => 9]]
]] ]]
@ -460,11 +460,11 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
[ [
'data' => ['first' => 'ONE', 'second' => 'TWO', 'third' => null], 'data' => ['first' => 'ONE', 'second' => 'TWO', 'third' => null],
'errors' => [[ 'errors' => [[
'message' => 'Expected a value of type "SimpleEnum" but received: "WRONG"', 'debugMessage' => 'Expected a value of type "SimpleEnum" but received: "WRONG"',
'locations' => [['line' => 4, 'column' => 13]] 'locations' => [['line' => 4, 'column' => 13]]
]] ]]
], ],
GraphQL::execute($this->schema, $q) GraphQL::executeAndReturnResult($this->schema, $q)->toArray(true)
); );
} }

View File

@ -117,14 +117,14 @@ class ScalarSerializationTest extends \PHPUnit_Framework_TestCase
try { try {
$floatType->serialize('one'); $floatType->serialize('one');
$this->fail('Expected exception was not thrown'); $this->fail('Expected exception was not thrown');
} catch (UserError $e) { } catch (InvariantViolation $e) {
$this->assertEquals('Float cannot represent non numeric value: "one"', $e->getMessage()); $this->assertEquals('Float cannot represent non numeric value: "one"', $e->getMessage());
} }
try { try {
$floatType->serialize(''); $floatType->serialize('');
$this->fail('Expected exception was not thrown'); $this->fail('Expected exception was not thrown');
} catch (UserError $e) { } catch (InvariantViolation $e) {
$this->assertEquals('Float cannot represent non numeric value: (empty string)', $e->getMessage()); $this->assertEquals('Float cannot represent non numeric value: (empty string)', $e->getMessage());
} }
@ -149,14 +149,14 @@ class ScalarSerializationTest extends \PHPUnit_Framework_TestCase
try { try {
$stringType->serialize([]); $stringType->serialize([]);
$this->fail('Expected exception was not thrown'); $this->fail('Expected exception was not thrown');
} catch (UserError $e) { } catch (InvariantViolation $e) {
$this->assertEquals('String cannot represent non scalar value: array', $e->getMessage()); $this->assertEquals('String cannot represent non scalar value: array(0)', $e->getMessage());
} }
try { try {
$stringType->serialize(new \stdClass()); $stringType->serialize(new \stdClass());
$this->fail('Expected exception was not thrown'); $this->fail('Expected exception was not thrown');
} catch (UserError $e) { } catch (InvariantViolation $e) {
$this->assertEquals('String cannot represent non scalar value: instance of stdClass', $e->getMessage()); $this->assertEquals('String cannot represent non scalar value: instance of stdClass', $e->getMessage());
} }
} }