Skip to content

Commit 38922db

Browse files
committed
Default error formatter now returns "Internal server error" unless error is client-aware and safe to report directly to end-users
1 parent fbcd208 commit 38922db

17 files changed

+357
-156
lines changed

UPGRADE.md

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,43 @@
11
# Upgrade
22

33
## Upgrade v0.9.x > v0.10.x
4-
### Deprecated: GraphQL\Utils moved to GraphQL\Utils\Utils
5-
Old class still exists, but triggers deprecation warning.
4+
#### Breaking: default error formatting
5+
By default exceptions thrown in resolvers will be reported with generic message `"Internal server error"`.
6+
Only exceptions implementing interface `GraphQL\Error\ClientAware` and claiming themselves as `safe` will
7+
be reported with full error message.
8+
9+
This is done to avoid information leak in production when unhandled exceptions occur in resolvers
10+
(e.g. database connection errors, file access errors, etc).
11+
12+
During development or debugging use `$executionResult->toArray(true)`. It will add `debugMessage` key to
13+
each error entry in result. If you also want to add `trace` for each error - pass flags instead:
14+
15+
```
16+
use GraphQL\Error\FormattedError;
17+
$debug = FormattedError::INCLUDE_DEBUG_MESSAGE | FormattedError::INCLUDE_TRACE;
18+
$result = GraphQL::executeAndReturnResult()->toArray($debug);
19+
```
20+
21+
To change default `"Internal server error"` message to something else, use:
22+
```
23+
GraphQL\Error\FormattedError::setInternalErrorMessage("Unexpected error");
24+
```
25+
26+
**This change only affects default error reporting mechanism. If you set your own error formatter using
27+
`ExecutionResult::setErrorFormatter()` you won't be affected by this change.**
28+
29+
If you are not happy with this change just set [your own error
30+
formatter](http://webonyx.github.io/graphql-php/error-handling/#custom-error-formatting).
31+
32+
#### Deprecated: GraphQL\Utils moved to GraphQL\Utils\Utils
33+
Old class still exists, but triggers deprecation warning when referenced.
634

735
## Upgrade v0.7.x > v0.8.x
836
All of those changes apply to those who extends various parts of this library.
937
If you only use the library and don't try to extend it - everything should work without breaks.
1038

1139

12-
### Breaking: Custom directives handling
40+
#### Breaking: Custom directives handling
1341
When passing custom directives to schema, default directives (like `@skip` and `@include`)
1442
are not added to schema automatically anymore. If you need them - add them explicitly with
1543
your other directives.
@@ -30,15 +58,15 @@ $schema = new Schema([
3058
]);
3159
```
3260

33-
### Breaking: Schema protected property and methods visibility
61+
#### Breaking: Schema protected property and methods visibility
3462
Most of the `protected` properties and methods of `GraphQL\Schema` were changed to `private`.
3563
Please use public interface instead.
3664

37-
### Breaking: Node kind constants
65+
#### Breaking: Node kind constants
3866
Node kind constants were extracted from `GraphQL\Language\AST\Node` to
3967
separate class `GraphQL\Language\AST\NodeKind`
4068

41-
### Non-breaking: AST node classes renamed
69+
#### Non-breaking: AST node classes renamed
4270
AST node classes were renamed to disambiguate with types. e.g.:
4371

4472
```
@@ -50,7 +78,7 @@ etc.
5078
Old names are still available via `class_alias` defined in `src/deprecated.php`.
5179
This file is included automatically when using composer autoloading.
5280

53-
### Deprecations
81+
#### Deprecations
5482
There are several deprecations which still work, but trigger `E_USER_DEPRECATED` when used.
5583

5684
For example `GraphQL\Executor\Executor::setDefaultResolveFn()` is renamed to `setDefaultResolver()`
@@ -61,7 +89,7 @@ but still works with old name.
6189
There are a few new breaking changes in v0.7.0 that were added to the graphql-js reference implementation
6290
with the spec of April2016
6391

64-
### 1. Context for resolver
92+
#### 1. Context for resolver
6593

6694
You can now pass a custom context to the `GraphQL::execute` function that is available in all resolvers as 3rd argument.
6795
This can for example be used to pass the current user etc.
@@ -119,7 +147,7 @@ function resolveMyField($object, array $args, $context, ResolveInfo $info){
119147
}
120148
```
121149

122-
### 2. Schema constructor signature
150+
#### 2. Schema constructor signature
123151

124152
The signature of the Schema constructor now accepts an associative config array instead of positional arguments:
125153

@@ -137,7 +165,7 @@ $schema = new Schema([
137165
]);
138166
```
139167

140-
### 3. Types can be directly passed to schema
168+
#### 3. Types can be directly passed to schema
141169

142170
There are edge cases when GraphQL cannot infer some types from your schema.
143171
One example is when you define a field of interface type and object types implementing

docs/error-handling.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,28 @@ exceptions thrown by resolvers. To access original exceptions use `$error->getPr
113113
But note that previous exception is only available for **Execution** errors and will be `null`
114114
for **Syntax** or **Validation** errors.
115115

116+
For example:
117+
118+
```php
119+
$result = GraphQL::executeAndReturnResult()
120+
->setErrorFormatter(function(GraphQL\Error\Error $err) {
121+
$resolverException = $err->getPrevious();
122+
123+
if ($resolverException instanceof MyResolverException) {
124+
$formattedError = [
125+
'message' => $resolverException->getMessage(),
126+
'code' => $resolverException->getCode()
127+
];
128+
} else {
129+
$formattedError = [
130+
'message' => $err->getMessage()
131+
];
132+
}
133+
return $formattedError;
134+
})
135+
->toArray();
136+
```
137+
116138
# Schema Errors
117139
So far we only covered errors which occur during query execution process. But schema definition can
118140
also throw if there is an error in one of type definitions.

src/Error/ClientAware.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
namespace GraphQL\Error;
3+
4+
/**
5+
* Interface ClientAware
6+
*
7+
* @package GraphQL\Error
8+
*/
9+
interface ClientAware
10+
{
11+
/**
12+
* Returns true when exception message is safe to be displayed to client
13+
*
14+
* @return bool
15+
*/
16+
public function isClientSafe();
17+
}

src/Error/Error.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*
1515
* @package GraphQL
1616
*/
17-
class Error extends \Exception implements \JsonSerializable
17+
class Error extends \Exception implements \JsonSerializable, ClientAware
1818
{
1919
/**
2020
* A message describing the Error for debugging purposes.
@@ -62,6 +62,11 @@ class Error extends \Exception implements \JsonSerializable
6262
*/
6363
private $positions;
6464

65+
/**
66+
* @var bool
67+
*/
68+
private $isClientSafe;
69+
6570
/**
6671
* Given an arbitrary Error, presumably thrown while attempting to execute a
6772
* GraphQL operation, produce a new GraphQLError aware of the location in the
@@ -133,6 +138,22 @@ public function __construct($message, $nodes = null, Source $source = null, $pos
133138
$this->source = $source;
134139
$this->positions = $positions;
135140
$this->path = $path;
141+
142+
if ($previous instanceof ClientAware) {
143+
$this->isClientSafe = $previous->isClientSafe();
144+
} else if ($previous === null) {
145+
$this->isClientSafe = true;
146+
} else {
147+
$this->isClientSafe = false;
148+
}
149+
}
150+
151+
/**
152+
* @return bool
153+
*/
154+
public function isClientSafe()
155+
{
156+
return $this->isClientSafe;
136157
}
137158

138159
/**
@@ -190,6 +211,7 @@ public function getLocations()
190211
/**
191212
* Returns array representation of error suitable for serialization
192213
*
214+
* @deprecated Use FormattedError::createFromException() instead
193215
* @return array
194216
*/
195217
public function toSerializableArray()

src/Error/FormattedError.php

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,84 @@
1313
*/
1414
class FormattedError
1515
{
16+
const INCLUDE_DEBUG_MESSAGE = 1;
17+
const INCLUDE_TRACE = 2;
18+
19+
private static $internalErrorMessage = 'Internal server error';
20+
21+
public static function setInternalErrorMessage($msg)
22+
{
23+
self::$internalErrorMessage = $msg;
24+
}
25+
1626
/**
1727
* @param \Throwable $e
18-
* @param $debug
28+
* @param bool|int $debug
29+
* @param string $internalErrorMessage
1930
*
2031
* @return array
2132
*/
22-
public static function createFromException($e, $debug = false)
33+
public static function createFromException($e, $debug = false, $internalErrorMessage = null)
2334
{
24-
if ($e instanceof Error) {
25-
$result = $e->toSerializableArray();
26-
} else if ($e instanceof \ErrorException) {
35+
Utils::invariant(
36+
$e instanceof \Exception || $e instanceof \Throwable,
37+
"Expected exception, got %s",
38+
Utils::getVariableType($e)
39+
);
40+
41+
$debug = (int) $debug;
42+
$internalErrorMessage = $internalErrorMessage ?: self::$internalErrorMessage;
43+
44+
if ($e instanceof ClientAware) {
45+
if ($e->isClientSafe()) {
46+
$result = [
47+
'message' => $e->getMessage()
48+
];
49+
} else {
50+
$result = [
51+
'message' => $internalErrorMessage,
52+
'isInternalError' => true
53+
];
54+
}
55+
} else {
2756
$result = [
28-
'message' => $e->getMessage(),
57+
'message' => $internalErrorMessage,
58+
'isInternalError' => true
2959
];
60+
}
61+
if (($debug & self::INCLUDE_DEBUG_MESSAGE > 0) && $result['message'] === $internalErrorMessage) {
62+
$result['debugMessage'] = $e->getMessage();
63+
}
64+
65+
if ($e instanceof Error) {
66+
$locations = Utils::map($e->getLocations(), function(SourceLocation $loc) {
67+
return $loc->toSerializableArray();
68+
});
69+
70+
if (!empty($locations)) {
71+
$result['locations'] = $locations;
72+
}
73+
if (!empty($e->path)) {
74+
$result['path'] = $e->path;
75+
}
76+
} else if ($e instanceof \ErrorException) {
3077
if ($debug) {
3178
$result += [
3279
'file' => $e->getFile(),
3380
'line' => $e->getLine(),
3481
'severity' => $e->getSeverity()
3582
];
3683
}
37-
} else {
38-
Utils::invariant(
39-
$e instanceof \Exception || $e instanceof \Throwable,
40-
"Expected exception, got %s",
41-
Utils::getVariableType($e)
42-
);
43-
$result = [
44-
'message' => $e->getMessage()
45-
];
84+
} else if ($e instanceof \Error) {
85+
if ($debug) {
86+
$result += [
87+
'file' => $e->getFile(),
88+
'line' => $e->getLine(),
89+
];
90+
}
4691
}
4792

48-
if ($debug) {
93+
if ($debug & self::INCLUDE_TRACE > 0) {
4994
$debugging = $e->getPrevious() ?: $e;
5095
$result['trace'] = static::toSafeTrace($debugging->getTrace());
5196
}

src/Error/UserError.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@
44
/**
55
* Class UserError
66
*
7-
* Error that can be safely displayed to client...
7+
* Error caused by actions of GraphQL clients. Can be safely displayed to client...
88
*
99
* @package GraphQL\Error
1010
*/
11-
class UserError extends InvariantViolation
11+
class UserError extends \RuntimeException implements ClientAware
1212
{
13+
/**
14+
* @return bool
15+
*/
16+
public function isClientSafe()
17+
{
18+
return true;
19+
}
1320
}

src/Executor/ExecutionResult.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
namespace GraphQL\Executor;
33

44
use GraphQL\Error\Error;
5+
use GraphQL\Error\FormattedError;
56

67
class ExecutionResult implements \JsonSerializable
78
{
@@ -23,7 +24,7 @@ class ExecutionResult implements \JsonSerializable
2324
/**
2425
* @var callable
2526
*/
26-
private $errorFormatter = ['GraphQL\Error\Error', 'formatError'];
27+
private $errorFormatter;
2728

2829
/**
2930
* @param array $data
@@ -48,14 +49,26 @@ public function setErrorFormatter(callable $errorFormatter)
4849
}
4950

5051
/**
52+
* @param bool|int $debug
5153
* @return array
5254
*/
53-
public function toArray()
55+
public function toArray($debug = false)
5456
{
5557
$result = [];
5658

5759
if (!empty($this->errors)) {
58-
$result['errors'] = array_map($this->errorFormatter, $this->errors);
60+
if ($debug) {
61+
$errorFormatter = function($e) use ($debug) {
62+
return FormattedError::createFromException($e, $debug);
63+
};
64+
} else if (!$this->errorFormatter) {
65+
$errorFormatter = function($e) {
66+
return FormattedError::createFromException($e, false);
67+
};
68+
} else {
69+
$errorFormatter = $this->errorFormatter;
70+
}
71+
$result['errors'] = array_map($errorFormatter, $this->errors);
5972
}
6073

6174
if (null !== $this->data) {

tests/Executor/AbstractPromiseTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
namespace GraphQL\Tests\Executor;
33

44
use GraphQL\Deferred;
5+
use GraphQL\Error\UserError;
56
use GraphQL\Error\Warning;
67
use GraphQL\GraphQL;
78
use GraphQL\Schema;
@@ -120,7 +121,7 @@ public function testIsTypeOfCanBeRejected()
120121
'interfaces' => [$PetType],
121122
'isTypeOf' => function () {
122123
return new Deferred(function () {
123-
throw new \Exception('We are testing this error');
124+
throw new UserError('We are testing this error');
124125
});
125126
},
126127
'fields' => [
@@ -578,7 +579,7 @@ public function testResolveTypeCanBeCaught()
578579
'name' => 'Pet',
579580
'resolveType' => function () {
580581
return new Deferred(function () {
581-
throw new \Exception('We are testing this error');
582+
throw new UserError('We are testing this error');
582583
});
583584
},
584585
'fields' => [

0 commit comments

Comments
 (0)