Skip to content

Commit ac17154

Browse files
authored
Merge pull request #4029 from magento-honey-badgers/query-get-request-support
[honey] Graphql-229: Query get request support
2 parents 450d998 + 8ffc06e commit ac17154

File tree

51 files changed

+674
-287
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+674
-287
lines changed

app/code/Magento/GraphQl/Controller/GraphQl.php

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Magento\Framework\App\RequestInterface;
1313
use Magento\Framework\App\ResponseInterface;
1414
use Magento\Framework\GraphQl\Exception\ExceptionFormatter;
15+
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
1516
use Magento\Framework\GraphQl\Query\QueryProcessor;
1617
use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
1718
use Magento\Framework\GraphQl\Schema\SchemaGeneratorInterface;
@@ -47,12 +48,12 @@ class GraphQl implements FrontControllerInterface
4748
private $queryProcessor;
4849

4950
/**
50-
* @var \Magento\Framework\GraphQl\Exception\ExceptionFormatter
51+
* @var ExceptionFormatter
5152
*/
5253
private $graphQlError;
5354

5455
/**
55-
* @var \Magento\Framework\GraphQl\Query\Resolver\ContextInterface
56+
* @var ContextInterface
5657
*/
5758
private $resolverContext;
5859

@@ -71,8 +72,8 @@ class GraphQl implements FrontControllerInterface
7172
* @param SchemaGeneratorInterface $schemaGenerator
7273
* @param SerializerInterface $jsonSerializer
7374
* @param QueryProcessor $queryProcessor
74-
* @param \Magento\Framework\GraphQl\Exception\ExceptionFormatter $graphQlError
75-
* @param \Magento\Framework\GraphQl\Query\Resolver\ContextInterface $resolverContext
75+
* @param ExceptionFormatter $graphQlError
76+
* @param ContextInterface $resolverContext
7677
* @param HttpRequestProcessor $requestProcessor
7778
* @param QueryFields $queryFields
7879
*/
@@ -107,12 +108,14 @@ public function dispatch(RequestInterface $request) : ResponseInterface
107108
$statusCode = 200;
108109
try {
109110
/** @var Http $request */
111+
$this->requestProcessor->validateRequest($request);
110112
$this->requestProcessor->processHeaders($request);
111-
$data = $this->jsonSerializer->unserialize($request->getContent());
112113

113-
$query = isset($data['query']) ? $data['query'] : '';
114-
$variables = isset($data['variables']) ? $data['variables'] : null;
115-
// We have to extract queried field names to avoid instantiation of non necessary fields in webonyx schema
114+
$data = $this->getDataFromRequest($request);
115+
$query = $data['query'] ?? '';
116+
$variables = $data['variables'] ?? null;
117+
118+
// We must extract queried field names to avoid instantiation of unnecessary fields in webonyx schema
116119
// Temporal coupling is required for performance optimization
117120
$this->queryFields->setQuery($query, $variables);
118121
$schema = $this->schemaGenerator->generate();
@@ -121,7 +124,7 @@ public function dispatch(RequestInterface $request) : ResponseInterface
121124
$schema,
122125
$query,
123126
$this->resolverContext,
124-
isset($data['variables']) ? $data['variables'] : []
127+
$data['variables'] ?? []
125128
);
126129
} catch (\Exception $error) {
127130
$result['errors'] = isset($result) && isset($result['errors']) ? $result['errors'] : [];
@@ -134,4 +137,26 @@ public function dispatch(RequestInterface $request) : ResponseInterface
134137
)->setHttpResponseCode($statusCode);
135138
return $this->response;
136139
}
140+
141+
/**
142+
* Get data from request body or query string
143+
*
144+
* @param RequestInterface $request
145+
* @return array
146+
*/
147+
private function getDataFromRequest(RequestInterface $request) : array
148+
{
149+
/** @var Http $request */
150+
if ($request->isPost()) {
151+
$data = $this->jsonSerializer->unserialize($request->getContent());
152+
} elseif ($request->isGet()) {
153+
$data = $request->getParams();
154+
$data['variables'] = isset($data['variables']) ?
155+
$this->jsonSerializer->unserialize($data['variables']) : null;
156+
} else {
157+
return [];
158+
}
159+
160+
return $data;
161+
}
137162
}

app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/ContentTypeProcessor.php

Lines changed: 0 additions & 32 deletions
This file was deleted.

app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/StoreProcessor.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
namespace Magento\GraphQl\Controller\HttpHeaderProcessor;
99

10-
use Magento\Framework\Exception\NoSuchEntityException;
10+
use Magento\Framework\App\HttpRequestInterface;
1111
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
1212
use Magento\GraphQl\Controller\HttpHeaderProcessorInterface;
1313
use Magento\Store\Model\StoreManagerInterface;
@@ -35,8 +35,9 @@ public function __construct(StoreManagerInterface $storeManager)
3535
/**
3636
* Handle the value of the store and set the scope
3737
*
38-
* {@inheritDoc}
39-
* @throws NoSuchEntityException
38+
* @param string $headerValue
39+
* @return void
40+
* @throws GraphQlInputException
4041
*/
4142
public function processHeaderValue(string $headerValue) : void
4243
{

app/code/Magento/GraphQl/Controller/HttpRequestProcessor.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,19 @@ class HttpRequestProcessor
1919
*/
2020
private $headerProcessors = [];
2121

22+
/**
23+
* @var HttpRequestValidatorInterface[] array
24+
*/
25+
private $requestValidators = [];
26+
2227
/**
2328
* @param HttpHeaderProcessorInterface[] $graphQlHeaders
29+
* @param HttpRequestValidatorInterface[] $requestValidators
2430
*/
25-
public function __construct(array $graphQlHeaders = [])
31+
public function __construct(array $graphQlHeaders = [], array $requestValidators = [])
2632
{
2733
$this->headerProcessors = $graphQlHeaders;
34+
$this->requestValidators = $requestValidators;
2835
}
2936

3037
/**
@@ -39,4 +46,17 @@ public function processHeaders(Http $request) : void
3946
$headerClass->processHeaderValue((string)$request->getHeader($headerName));
4047
}
4148
}
49+
50+
/**
51+
* Validate HTTP request
52+
*
53+
* @param Http $request
54+
* @return void
55+
*/
56+
public function validateRequest(Http $request) : void
57+
{
58+
foreach ($this->requestValidators as $requestValidator) {
59+
$requestValidator->validate($request);
60+
}
61+
}
4262
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\GraphQl\Controller\HttpRequestValidator;
9+
10+
use Magento\Framework\App\HttpRequestInterface;
11+
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
12+
use Magento\GraphQl\Controller\HttpRequestValidatorInterface;
13+
14+
/**
15+
* Processes the "Content-Type" header entry
16+
*/
17+
class ContentTypeValidator implements HttpRequestValidatorInterface
18+
{
19+
/**
20+
* Handle the mandatory application/json header
21+
*
22+
* @param HttpRequestInterface $request
23+
* @return void
24+
* @throws GraphQlInputException
25+
*/
26+
public function validate(HttpRequestInterface $request) : void
27+
{
28+
$headerName = 'Content-Type';
29+
$requiredHeaderValue = 'application/json';
30+
31+
$headerValue = (string)$request->getHeader($headerName);
32+
if ($request->isPost()
33+
&& strpos($headerValue, $requiredHeaderValue) === false
34+
) {
35+
throw new GraphQlInputException(
36+
new \Magento\Framework\Phrase('Request content type must be application/json')
37+
);
38+
}
39+
}
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\GraphQl\Controller\HttpRequestValidator;
9+
10+
use Magento\Framework\App\HttpRequestInterface;
11+
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
12+
use Magento\Framework\App\Request\Http;
13+
use Magento\GraphQl\Controller\HttpRequestValidatorInterface;
14+
15+
/**
16+
* Validator to check HTTP verb for Graphql requests
17+
*/
18+
class HttpVerbValidator implements HttpRequestValidatorInterface
19+
{
20+
/**
21+
* Check if request is using correct verb for query or mutation
22+
*
23+
* @param HttpRequestInterface $request
24+
* @return void
25+
* @throws GraphQlInputException
26+
*/
27+
public function validate(HttpRequestInterface $request) : void
28+
{
29+
/** @var Http $request */
30+
if (false === $request->isPost()) {
31+
$query = $request->getParam('query', '');
32+
// The easiest way to determine mutations without additional parsing
33+
if (strpos(trim($query), 'mutation') === 0) {
34+
throw new GraphQlInputException(
35+
new \Magento\Framework\Phrase('Mutation requests allowed only for POST requests')
36+
);
37+
}
38+
}
39+
}
40+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\GraphQl\Controller;
9+
10+
use Magento\Framework\App\HttpRequestInterface;
11+
12+
/**
13+
* Use this interface to implement a validator for a Graphql HTTP requests
14+
*/
15+
interface HttpRequestValidatorInterface
16+
{
17+
/**
18+
* Perform validation of request
19+
*
20+
* @param HttpRequestInterface $request
21+
* @return void
22+
*/
23+
public function validate(HttpRequestInterface $request) : void;
24+
}

app/code/Magento/GraphQl/etc/graphql/di.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@
2828
<type name="Magento\GraphQl\Controller\HttpRequestProcessor">
2929
<arguments>
3030
<argument name="graphQlHeaders" xsi:type="array">
31-
<item name="Content-Type" xsi:type="object">Magento\GraphQl\Controller\HttpHeaderProcessor\ContentTypeProcessor</item>
3231
<item name="Store" xsi:type="object">Magento\GraphQl\Controller\HttpHeaderProcessor\StoreProcessor</item>
3332
</argument>
33+
<argument name="requestValidators" xsi:type="array">
34+
<item name="ContentTypeValidator" xsi:type="object">Magento\GraphQl\Controller\HttpRequestValidator\ContentTypeValidator</item>
35+
<item name="VerbValidator" xsi:type="object">Magento\GraphQl\Controller\HttpRequestValidator\HttpVerbValidator</item>
36+
</argument>
3437
</arguments>
3538
</type>
3639
</config>

dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/Client.php

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public function __construct(
5151
* @return array|string|int|float|bool
5252
* @throws \Exception
5353
*/
54-
public function postQuery(string $query, array $variables = [], string $operationName = '', array $headers = [])
54+
public function post(string $query, array $variables = [], string $operationName = '', array $headers = [])
5555
{
5656
$url = $this->getEndpointUrl();
5757
$headers = array_merge($headers, ['Accept: application/json', 'Content-Type: application/json']);
@@ -63,19 +63,57 @@ public function postQuery(string $query, array $variables = [], string $operatio
6363
$postData = $this->json->jsonEncode($requestArray);
6464

6565
$responseBody = $this->curlClient->post($url, $postData, $headers);
66-
$responseBodyArray = $this->json->jsonDecode($responseBody);
66+
return $this->processResponse($responseBody);
67+
}
68+
69+
/**
70+
* Perform HTTP GET request for query
71+
*
72+
* @param string $query
73+
* @param array $variables
74+
* @param string $operationName
75+
* @param array $headers
76+
* @return mixed
77+
* @throws \Exception
78+
*/
79+
public function get(string $query, array $variables = [], string $operationName = '', array $headers = [])
80+
{
81+
$url = $this->getEndpointUrl();
82+
$requestArray = [
83+
'query' => $query,
84+
'variables' => $variables ? $this->json->jsonEncode($variables) : null,
85+
'operationName' => $operationName ?? null
86+
];
87+
array_filter($requestArray);
88+
89+
$responseBody = $this->curlClient->get($url, $requestArray, $headers);
90+
return $this->processResponse($responseBody);
91+
}
92+
93+
/**
94+
* Process response from GraphQl server
95+
*
96+
* @param string $response
97+
* @return mixed
98+
* @throws \Exception
99+
*/
100+
private function processResponse(string $response)
101+
{
102+
$responseArray = $this->json->jsonDecode($response);
67103

68-
if (!is_array($responseBodyArray)) {
69-
throw new \Exception('Unknown GraphQL response body: ' . json_encode($responseBodyArray));
104+
if (!is_array($responseArray)) {
105+
//phpcs:ignore Magento2.Exceptions.DirectThrow
106+
throw new \Exception('Unknown GraphQL response body: ' . $response);
70107
}
71108

72-
$this->processErrors($responseBodyArray);
109+
$this->processErrors($responseArray);
73110

74-
if (!isset($responseBodyArray['data'])) {
75-
throw new \Exception('Unknown GraphQL response body: ' . json_encode($responseBodyArray));
76-
} else {
77-
return $responseBodyArray['data'];
111+
if (!isset($responseArray['data'])) {
112+
//phpcs:ignore Magento2.Exceptions.DirectThrow
113+
throw new \Exception('Unknown GraphQL response body: ' . $response);
78114
}
115+
116+
return $responseArray['data'];
79117
}
80118

81119
/**
@@ -107,6 +145,7 @@ private function processErrors($responseBodyArray)
107145
$responseBodyArray
108146
);
109147
}
148+
//phpcs:ignore Magento2.Exceptions.DirectThrow
110149
throw new \Exception('GraphQL responded with an unknown error: ' . json_encode($responseBodyArray));
111150
}
112151
}

0 commit comments

Comments
 (0)