Skip to content

Commit e6e531b

Browse files
committed
Server: throw only when there is a configuration or logic error (invariant violation)
1 parent 90088c7 commit e6e531b

File tree

8 files changed

+241
-242
lines changed

8 files changed

+241
-242
lines changed

src/Server/Helper.php

Lines changed: 75 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
use GraphQL\Error\Error;
55
use GraphQL\Error\FormattedError;
66
use GraphQL\Error\InvariantViolation;
7-
use GraphQL\Error\UserError;
87
use GraphQL\Executor\ExecutionResult;
98
use GraphQL\Executor\Executor;
109
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
@@ -36,7 +35,14 @@ class Helper
3635
public function executeOperation(ServerConfig $config, OperationParams $op)
3736
{
3837
$promiseAdapter = $config->getPromiseAdapter() ?: Executor::getPromiseAdapter();
39-
$result = $this->promiseToExecuteOperation($promiseAdapter, $config, $op);
38+
39+
$errors = $this->validateOperationParams($op);
40+
41+
if (!empty($errors)) {
42+
$result = $promiseAdapter->createFulfilled(new ExecutionResult(null, $errors));
43+
} else {
44+
$result = $this->promiseToExecuteOperation($promiseAdapter, $config, $op);
45+
}
4046

4147
if ($promiseAdapter instanceof SyncPromiseAdapter) {
4248
$result = $promiseAdapter->wait($result);
@@ -59,7 +65,12 @@ public function executeBatch(ServerConfig $config, array $operations)
5965
$result = [];
6066

6167
foreach ($operations as $operation) {
62-
$result[] = $this->promiseToExecuteOperation($promiseAdapter, $config, $operation);
68+
$errors = $this->validateOperationParams($operation);
69+
if (!empty($errors)) {
70+
$result[] = $promiseAdapter->createFulfilled(new ExecutionResult(null, $errors));
71+
} else {
72+
$result[] = $this->promiseToExecuteOperation($promiseAdapter, $config, $operation);
73+
}
6374
}
6475

6576
$result = $promiseAdapter->all($result);
@@ -79,63 +90,52 @@ public function executeBatch(ServerConfig $config, array $operations)
7990
*/
8091
private function promiseToExecuteOperation(PromiseAdapter $promiseAdapter, ServerConfig $config, OperationParams $op)
8192
{
82-
$phpErrors = [];
93+
try {
94+
$doc = $op->queryId ? static::loadPersistedQuery($config, $op) : $op->query;
8395

84-
$execute = function() use ($config, $op, $promiseAdapter) {
85-
try {
86-
$doc = $op->queryId ? static::loadPersistedQuery($config, $op) : $op->query;
96+
if (!$doc instanceof DocumentNode) {
97+
$doc = Parser::parse($doc);
98+
}
99+
if ($op->isReadOnly() && AST::getOperation($doc, $op->operation) !== 'query') {
100+
throw new Error("GET supports only query operation");
101+
}
87102

88-
if (!$doc instanceof DocumentNode) {
89-
$doc = Parser::parse($doc);
90-
}
91-
if ($op->isReadOnly() && AST::isMutation($op->operation, $doc)) {
92-
throw new UserError("Cannot execute mutation in read-only context");
93-
}
103+
$validationErrors = DocumentValidator::validate(
104+
$config->getSchema(),
105+
$doc,
106+
$this->resolveValidationRules($config, $op)
107+
);
94108

95-
$validationErrors = DocumentValidator::validate(
109+
if (!empty($validationErrors)) {
110+
$result = $promiseAdapter->createFulfilled(
111+
new ExecutionResult(null, $validationErrors)
112+
);
113+
} else {
114+
$result = Executor::promiseToExecute(
115+
$promiseAdapter,
96116
$config->getSchema(),
97117
$doc,
98-
$this->resolveValidationRules($config, $op)
99-
);
100-
101-
if (!empty($validationErrors)) {
102-
return $promiseAdapter->createFulfilled(
103-
new ExecutionResult(null, $validationErrors)
104-
);
105-
} else {
106-
return Executor::promiseToExecute(
107-
$promiseAdapter,
108-
$config->getSchema(),
109-
$doc,
110-
$config->getRootValue(),
111-
$config->getContext(),
112-
$op->variables,
113-
$op->operation,
114-
$config->getDefaultFieldResolver()
115-
);
116-
}
117-
} catch (Error $e) {
118-
return $promiseAdapter->createFulfilled(
119-
new ExecutionResult(null, [$e])
118+
$config->getRootValue(),
119+
$config->getContext(),
120+
$op->variables,
121+
$op->operation,
122+
$config->getDefaultFieldResolver()
120123
);
121124
}
122-
};
123-
if ($config->getDebug()) {
124-
$execute = Utils::withErrorHandling($execute, $phpErrors);
125+
} catch (Error $e) {
126+
$result = $promiseAdapter->createFulfilled(
127+
new ExecutionResult(null, [$e])
128+
);
125129
}
126-
$result = $execute();
127130

128-
$applyErrorFormatting = function (ExecutionResult $result) use ($config, $phpErrors) {
131+
$applyErrorFormatting = function (ExecutionResult $result) use ($config) {
129132
if ($config->getDebug()) {
130133
$errorFormatter = function($e) {
131134
return FormattedError::createFromException($e, true);
132135
};
133136
} else {
134137
$errorFormatter = $config->getErrorFormatter();
135138
}
136-
if (!empty($phpErrors)) {
137-
$result->extensions['phpErrors'] = array_map($errorFormatter, $phpErrors);
138-
}
139139
$result->setErrorFormatter($errorFormatter);
140140
return $result;
141141
};
@@ -160,7 +160,7 @@ public function loadPersistedQuery(ServerConfig $config, OperationParams $op)
160160
$loader = $config->getPersistentQueryLoader();
161161

162162
if (!$loader) {
163-
throw new UserError("Persisted queries are not supported by this server");
163+
throw new Error("Persisted queries are not supported by this server");
164164
}
165165

166166
$source = $loader($op->queryId, $op);
@@ -190,10 +190,10 @@ public function resolveValidationRules(ServerConfig $config, OperationParams $pa
190190
$validationRules = $validationRules($params);
191191

192192
if (!is_array($validationRules)) {
193-
throw new InvariantViolation(
194-
"Validation rules callable must return array of rules, but got: %s" .
193+
throw new InvariantViolation(sprintf(
194+
"Expecting validation rules to be array or callable returning array, but got: %s",
195195
Utils::printSafe($validationRules)
196-
);
196+
));
197197
}
198198
}
199199

@@ -210,19 +210,14 @@ public function resolveValidationRules(ServerConfig $config, OperationParams $pa
210210
*
211211
* @param callable|null $readRawBodyFn
212212
* @return OperationParams|OperationParams[]
213+
* @throws Error
213214
*/
214215
public function parseHttpRequest(callable $readRawBodyFn = null)
215216
{
216217
$method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null;
217218

218219
if ($method === 'GET') {
219-
$request = array_change_key_case($_GET);
220-
221-
if (isset($request['query']) || isset($request['queryid']) || isset($request['documentid'])) {
222-
$result = OperationParams::create($_GET, true);
223-
} else {
224-
throw new UserError('Cannot execute GET request without "query" or "queryId" parameter');
225-
}
220+
$result = OperationParams::create($_GET, true);
226221
} else if ($method === 'POST') {
227222
$contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : null;
228223

@@ -234,18 +229,18 @@ public function parseHttpRequest(callable $readRawBodyFn = null)
234229
$body = json_decode($rawBody ?: '', true);
235230

236231
if (json_last_error()) {
237-
throw new UserError("Could not parse JSON: " . json_last_error_msg());
232+
throw new Error("Could not parse JSON: " . json_last_error_msg());
238233
}
239234
if (!is_array($body)) {
240-
throw new UserError(
235+
throw new Error(
241236
"GraphQL Server expects JSON object or array, but got " .
242237
Utils::printSafeJson($body)
243238
);
244239
}
245240
if (isset($body[0])) {
246241
$result = [];
247242
foreach ($body as $index => $entry) {
248-
$op = OperationParams::create($entry, true);
243+
$op = OperationParams::create($entry);
249244
$result[] = $op;
250245
}
251246
} else {
@@ -254,12 +249,12 @@ public function parseHttpRequest(callable $readRawBodyFn = null)
254249
} else if (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
255250
$result = OperationParams::create($_POST);
256251
} else if (null === $contentType) {
257-
throw new UserError('Missing "Content-Type" header');
252+
throw new Error('Missing "Content-Type" header');
258253
} else {
259-
throw new UserError("Unexpected content type: " . Utils::printSafeJson($contentType));
254+
throw new Error("Unexpected content type: " . Utils::printSafeJson($contentType));
260255
}
261256
} else {
262-
throw new UserError('HTTP Method "' . $method . '" is not supported', 405);
257+
throw new Error('HTTP Method "' . $method . '" is not supported');
263258
}
264259
return $result;
265260
}
@@ -276,77 +271,43 @@ public function readRawBody()
276271
* Checks validity of operation params and returns array of errors (empty array when params are valid)
277272
*
278273
* @param OperationParams $params
279-
* @return array
274+
* @return Error[]
280275
*/
281276
public function validateOperationParams(OperationParams $params)
282277
{
283278
$errors = [];
284279
if (!$params->query && !$params->queryId) {
285-
$errors[] = 'GraphQL Request must include at least one of those two parameters: "query" or "queryId"';
280+
$errors[] = new Error('GraphQL Request must include at least one of those two parameters: "query" or "queryId"');
286281
}
287282
if ($params->query && $params->queryId) {
288-
$errors[] = 'GraphQL Request parameters "query" and "queryId" are mutually exclusive';
283+
$errors[] = new Error('GraphQL Request parameters "query" and "queryId" are mutually exclusive');
289284
}
290285

291286
if ($params->query !== null && (!is_string($params->query) || empty($params->query))) {
292-
$errors[] = 'GraphQL Request parameter "query" must be string, but got ' .
293-
Utils::printSafeJson($params->query);
287+
$errors[] = new Error(
288+
'GraphQL Request parameter "query" must be string, but got ' .
289+
Utils::printSafeJson($params->query)
290+
);
294291
}
295292
if ($params->queryId !== null && (!is_string($params->queryId) || empty($params->queryId))) {
296-
$errors[] = 'GraphQL Request parameter "queryId" must be string, but got ' .
297-
Utils::printSafeJson($params->queryId);
293+
$errors[] = new Error(
294+
'GraphQL Request parameter "queryId" must be string, but got ' .
295+
Utils::printSafeJson($params->queryId)
296+
);
298297
}
299298

300299
if ($params->operation !== null && (!is_string($params->operation) || empty($params->operation))) {
301-
$errors[] = 'GraphQL Request parameter "operation" must be string, but got ' .
302-
Utils::printSafeJson($params->operation);
300+
$errors[] = new Error(
301+
'GraphQL Request parameter "operation" must be string, but got ' .
302+
Utils::printSafeJson($params->operation)
303+
);
303304
}
304305
if ($params->variables !== null && (!is_array($params->variables) || isset($params->variables[0]))) {
305-
$errors[] = 'GraphQL Request parameter "variables" must be object, but got ' .
306-
Utils::printSafeJson($params->variables);
306+
$errors[] = new Error(
307+
'GraphQL Request parameter "variables" must be object or JSON string parsed to object, but got ' .
308+
Utils::printSafeJson($params->getOriginalInput('variables'))
309+
);
307310
}
308311
return $errors;
309312
}
310-
311-
/**
312-
* Assertion to check that parsed body is valid instance of OperationParams (or array of instances)
313-
*
314-
* @param OperationParams|OperationParams[] $parsedBody
315-
* @throws InvariantViolation
316-
* @throws UserError
317-
*/
318-
public function assertValidRequest($parsedBody)
319-
{
320-
if (is_array($parsedBody)) {
321-
foreach ($parsedBody as $index => $entry) {
322-
if (!$entry instanceof OperationParams) {
323-
throw new InvariantViolation(sprintf(
324-
'GraphQL Server: Parsed http request must be an instance of %s or array of such instances, '.
325-
'but got invalid array where entry at position %d is %s',
326-
OperationParams::class,
327-
$index,
328-
Utils::printSafe($entry)
329-
));
330-
}
331-
332-
$errors = $this->validateOperationParams($entry);
333-
334-
if (!empty($errors[0])) {
335-
$err = $index ? "Error in query #$index: {$errors[0]}" : $errors[0];
336-
throw new UserError($err);
337-
}
338-
}
339-
} else if ($parsedBody instanceof OperationParams) {
340-
$errors = $this->validateOperationParams($parsedBody);
341-
if (!empty($errors[0])) {
342-
throw new UserError($errors[0]);
343-
}
344-
} else {
345-
throw new InvariantViolation(sprintf(
346-
'GraphQL Server: Parsed http request must be an instance of %s or array of such instances, but got %s',
347-
OperationParams::class,
348-
Utils::printSafe($parsedBody)
349-
));
350-
}
351-
}
352313
}

src/Server/OperationParams.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
<?php
22
namespace GraphQL\Server;
33

4-
use GraphQL\Utils\Utils;
5-
64
/**
75
* Class QueryParams
86
* Represents all available parsed query parameters
@@ -52,9 +50,9 @@ class OperationParams
5250
public static function create(array $params, $readonly = false)
5351
{
5452
$instance = new static();
55-
$instance->originalInput = $params;
5653

5754
$params = array_change_key_case($params, CASE_LOWER);
55+
$instance->originalInput = $params;
5856

5957
$params += [
6058
'query' => null,
@@ -64,6 +62,13 @@ public static function create(array $params, $readonly = false)
6462
'variables' => null
6563
];
6664

65+
if (is_string($params['variables'])) {
66+
$tmp = json_decode($params['variables'], true);
67+
if (!json_last_error()) {
68+
$params['variables'] = $tmp;
69+
}
70+
}
71+
6772
$instance->query = $params['query'];
6873
$instance->queryId = $params['queryid'] ?: $params['documentid'];
6974
$instance->operation = $params['operation'];
@@ -74,11 +79,12 @@ public static function create(array $params, $readonly = false)
7479
}
7580

7681
/**
77-
* @return array
82+
* @param string $key
83+
* @return mixed
7884
*/
79-
public function getOriginalInput()
85+
public function getOriginalInput($key)
8086
{
81-
return $this->originalInput;
87+
return isset($this->originalInput[$key]) ? $this->originalInput[$key] : null;
8288
}
8389

8490
/**

src/Server/StandardServer.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22
namespace GraphQL\Server;
33

4+
use GraphQL\Error\InvariantViolation;
45
use GraphQL\Executor\ExecutionResult;
56
use GraphQL\Executor\Promise\Promise;
67

@@ -48,13 +49,13 @@ protected function __construct(ServerConfig $config)
4849
/**
4950
* @param OperationParams|OperationParams[] $parsedBody
5051
* @return ExecutionResult|ExecutionResult[]|Promise
52+
* @throws InvariantViolation
5153
*/
5254
public function executeRequest($parsedBody = null)
5355
{
5456
if (null !== $parsedBody) {
5557
$parsedBody = $this->helper->parseHttpRequest();
5658
}
57-
$this->helper->assertValidRequest($parsedBody);
5859

5960
if (is_array($parsedBody)) {
6061
return $this->helper->executeBatch($this->config, $parsedBody);

0 commit comments

Comments
 (0)