Skip to content

Commit c7f16fb

Browse files
committed
Merge remote-tracking branch 'origin/ACP2E-4194' into PR_2025_09_15_muntianu
2 parents 402b50b + a3dc81f commit c7f16fb

File tree

11 files changed

+223
-67
lines changed

11 files changed

+223
-67
lines changed

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

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Exception;
1111
use GraphQL\Error\FormattedError;
1212
use GraphQL\Error\SyntaxError;
13+
use GraphQL\Language\Source;
1314
use Magento\Framework\App\Area;
1415
use Magento\Framework\App\AreaList;
1516
use Magento\Framework\App\FrontControllerInterface;
@@ -23,6 +24,7 @@
2324
use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException;
2425
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
2526
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
27+
use Magento\Framework\GraphQl\Exception\InvalidRequestInterface;
2628
use Magento\Framework\GraphQl\Query\Fields as QueryFields;
2729
use Magento\Framework\GraphQl\Query\QueryParser;
2830
use Magento\Framework\GraphQl\Query\QueryProcessor;
@@ -33,7 +35,6 @@
3335
use Magento\GraphQl\Helper\Query\Logger\LogData;
3436
use Magento\GraphQl\Model\Query\ContextFactoryInterface;
3537
use Magento\GraphQl\Model\Query\Logger\LoggerPool;
36-
use Throwable;
3738

3839
/**
3940
* Front controller for web API GraphQL area.
@@ -239,25 +240,21 @@ public function dispatch(RequestInterface $request): ResponseInterface
239240
/**
240241
* Handle GraphQL Exceptions
241242
*
242-
* @param Exception $error
243+
* @param Exception $e
243244
* @return array
244-
* @throws Throwable
245245
*/
246-
private function handleGraphQlException(Exception $error): array
246+
private function handleGraphQlException(Exception $e): array
247247
{
248-
if ($error instanceof SyntaxError || $error instanceof GraphQlInputException) {
249-
return [['errors' => [FormattedError::createFromException($error)]], 400];
250-
}
251-
if ($error instanceof GraphQlAuthenticationException) {
252-
return [['errors' => [$this->graphQlError->create($error)]], 401];
253-
}
254-
if ($error instanceof GraphQlAuthorizationException) {
255-
return [['errors' => [$this->graphQlError->create($error)]], 403];
256-
}
257-
return [
258-
['errors' => [$this->graphQlError->create($error)]],
259-
ExceptionFormatter::HTTP_GRAPH_QL_SCHEMA_ERROR_STATUS
260-
];
248+
[$error, $statusCode] = match (true) {
249+
$e instanceof InvalidRequestInterface => [FormattedError::createFromException($e), $e->getStatusCode()],
250+
$e instanceof SyntaxError => [FormattedError::createFromException($e), 400],
251+
$e instanceof GraphQlAuthenticationException => [$this->graphQlError->create($e), 401],
252+
$e instanceof GraphQlAuthorizationException => [$this->graphQlError->create($e), 403],
253+
$e instanceof GraphQlInputException => [FormattedError::createFromException($e), 200],
254+
default => [$this->graphQlError->create($e), ExceptionFormatter::HTTP_GRAPH_QL_SCHEMA_ERROR_STATUS],
255+
};
256+
257+
return [['errors' => [$error]], $statusCode];
261258
}
262259

263260
/**
@@ -286,23 +283,30 @@ private function getHttpResponseCode(array $result): int
286283
*
287284
* @param RequestInterface $request
288285
* @return array
289-
* @throws GraphQlInputException
286+
* @throws SyntaxError
290287
*/
291288
private function getDataFromRequest(RequestInterface $request): array
292289
{
293290
$data = [];
294-
try {
295-
/** @var Http $request */
296-
if ($request->isPost()) {
297-
$data = $request->getContent() ? $this->jsonSerializer->unserialize($request->getContent()) : [];
298-
} elseif ($request->isGet()) {
299-
$data = $request->getParams();
291+
/** @var Http $request */
292+
if ($request->isPost() && $request->getContent()) {
293+
$content = $request->getContent();
294+
try {
295+
$data = $this->jsonSerializer->unserialize($content);
296+
} catch (\InvalidArgumentException) {
297+
$source = new Source($content);
298+
throw new SyntaxError($source, 0, 'Unable to parse the request.');
299+
}
300+
} elseif ($request->isGet()) {
301+
$data = $request->getParams();
302+
try {
300303
$data['variables'] = !empty($data['variables']) && is_string($data['variables'])
301304
? $this->jsonSerializer->unserialize($data['variables'])
302305
: null;
306+
} catch (\InvalidArgumentException) {
307+
$source = new Source($data['variables']);
308+
throw new SyntaxError($source, 0, 'Unable to parse the variables.');
303309
}
304-
} catch (\InvalidArgumentException $e) {
305-
throw new GraphQlInputException(__('Unable to parse the request.'), $e);
306310
}
307311

308312
return $data;

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2019 Adobe
4+
* All Rights Reserved.
55
*/
66
declare(strict_types=1);
77

88
namespace Magento\GraphQl\Controller\HttpRequestValidator;
99

1010
use Magento\Framework\App\HttpRequestInterface;
11-
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
11+
use Magento\Framework\GraphQl\Exception\UnsupportedMediaTypeException;
12+
use Magento\Framework\Phrase;
1213
use Magento\GraphQl\Controller\HttpRequestValidatorInterface;
1314

1415
/**
@@ -21,7 +22,7 @@ class ContentTypeValidator implements HttpRequestValidatorInterface
2122
*
2223
* @param HttpRequestInterface $request
2324
* @return void
24-
* @throws GraphQlInputException
25+
* @throws UnsupportedMediaTypeException
2526
*/
2627
public function validate(HttpRequestInterface $request) : void
2728
{
@@ -32,8 +33,8 @@ public function validate(HttpRequestInterface $request) : void
3233
if ($request->isPost()
3334
&& strpos($headerValue, $requiredHeaderValue) === false
3435
) {
35-
throw new GraphQlInputException(
36-
new \Magento\Framework\Phrase('Request content type must be application/json')
36+
throw new UnsupportedMediaTypeException(
37+
new Phrase('Request content type must be application/json')
3738
);
3839
}
3940
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2019 Adobe
4+
* All Rights Reserved.
55
*/
66
declare(strict_types=1);
77

@@ -13,7 +13,7 @@
1313
use Magento\Framework\App\HttpRequestInterface;
1414
use Magento\Framework\App\ObjectManager;
1515
use Magento\Framework\App\Request\Http;
16-
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
16+
use Magento\Framework\GraphQl\Exception\MethodNotAllowedException;
1717
use Magento\Framework\GraphQl\Query\QueryParser;
1818
use Magento\Framework\Phrase;
1919
use Magento\GraphQl\Controller\HttpRequestValidatorInterface;
@@ -41,7 +41,7 @@ public function __construct(?QueryParser $queryParser = null)
4141
*
4242
* @param HttpRequestInterface $request
4343
* @return void
44-
* @throws GraphQlInputException
44+
* @throws MethodNotAllowedException
4545
*/
4646
public function validate(HttpRequestInterface $request): void
4747
{
@@ -63,7 +63,7 @@ public function validate(HttpRequestInterface $request): void
6363
);
6464

6565
if ($operationType !== null && strtolower($operationType) === 'mutation') {
66-
throw new GraphQlInputException(
66+
throw new MethodNotAllowedException(
6767
new Phrase('Mutation requests allowed only for POST requests')
6868
);
6969
}

app/design/frontend/Magento/blank/i18n/en_US.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,3 +441,4 @@ Test,Test
441441
test,test
442442
Two,Two
443443
"Invalid data type","Invalid data type"
444+
"Unknown type ""%1"".","Unknown type ""%1""."

app/design/frontend/Magento/luma/i18n/en_US.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,4 @@ Test,Test
491491
test,test
492492
Two,Two
493493
"Invalid data type","Invalid data type"
494+
"Unknown type ""%1"".","Unknown type ""%1""."

dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,28 @@
99

1010
use Magento\Catalog\Api\Data\ProductInterface;
1111
use Magento\Catalog\Api\ProductRepositoryInterface;
12+
use Magento\Framework\App\Area;
1213
use Magento\Framework\App\Request\Http;
1314
use Magento\Framework\EntityManager\MetadataPool;
15+
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
1416
use Magento\Framework\Serialize\SerializerInterface;
17+
use Magento\TestFramework\Fixture\AppArea;
18+
use Magento\TestFramework\Fixture\DataFixture;
19+
use Magento\TestFramework\Fixture\DbIsolation;
1520
use Magento\TestFramework\Helper\Bootstrap;
21+
use PHPUnit\Framework\Attributes\CoversClass;
1622

1723
/**
1824
* Tests the dispatch method in the GraphQl Controller class using a simple product query
1925
*
20-
* @magentoAppArea graphql
21-
* @magentoDataFixture Magento/Catalog/_files/product_simple_with_url_key.php
22-
* @magentoDbIsolation disabled
2326
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
2427
*/
28+
#[
29+
CoversClass(GraphQl::class),
30+
AppArea(Area::AREA_GRAPHQL),
31+
DbIsolation(false),
32+
DataFixture('Magento/Catalog/_files/product_simple_with_url_key.php'),
33+
]
2534
class GraphQlControllerTest extends \Magento\TestFramework\Indexer\TestCase
2635
{
2736
/** @var \Magento\Framework\ObjectManagerInterface */
@@ -54,8 +63,8 @@ public static function setUpBeforeClass(): void
5463

5564
protected function setUp(): void
5665
{
57-
$this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
58-
$this->graphql = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class);
66+
$this->objectManager = Bootstrap::getObjectManager();
67+
$this->graphql = $this->objectManager->get(GraphQl::class);
5968
$this->jsonSerializer = $this->objectManager->get(SerializerInterface::class);
6069
$this->metadataPool = $this->objectManager->get(MetadataPool::class);
6170
$this->request = $this->objectManager->get(Http::class);
@@ -243,24 +252,17 @@ public function testError() : void
243252
->addHeaders(['Content-Type' => 'application/json']);
244253
$this->request->setHeaders($headers);
245254
$response = $this->graphql->dispatch($this->request);
255+
self::assertEquals(200, $response->getStatusCode());
256+
246257
$outputResponse = $this->jsonSerializer->unserialize($response->getContent());
247-
if (isset($outputResponse['errors'][0])) {
248-
if (is_array($outputResponse['errors'][0])) {
249-
foreach ($outputResponse['errors'] as $error) {
250-
$this->assertEquals(
251-
\Magento\Framework\GraphQl\Exception\GraphQlInputException::EXCEPTION_CATEGORY,
252-
$error['extensions']['category']
253-
);
254-
if (isset($error['message'])) {
255-
$this->assertEquals($error['message'], 'Invalid entity_type specified: invalid');
256-
}
257-
if (isset($error['trace'])) {
258-
if (is_array($error['trace'])) {
259-
$this->assertNotEmpty($error['trace']);
260-
}
261-
}
262-
}
263-
}
258+
self::assertArrayHasKey('errors', $outputResponse);
259+
self::assertNotEmpty($outputResponse['errors']);
260+
261+
$error = $outputResponse['errors'][0];
262+
self::assertEquals(GraphQlInputException::EXCEPTION_CATEGORY, $error['extensions']['category']);
263+
self::assertEquals('Invalid entity_type specified: invalid', $error['message']);
264+
if (isset($error['trace']) && is_array($error['trace'])) {
265+
self::assertNotEmpty($error['trace']);
264266
}
265267
}
266268

@@ -349,7 +351,7 @@ public function testDispatchPostWithInvalidJson(): void
349351
self::assertArrayHasKey('errors', $output);
350352
self::assertNotEmpty($output['errors']);
351353
self::assertArrayHasKey('message', $output['errors'][0]);
352-
self::assertEquals('Unable to parse the request.', $output['errors'][0]['message']);
354+
self::assertEquals('Syntax Error: Unable to parse the request.', $output['errors'][0]['message']);
353355
}
354356

355357
public function testDispatchPostWithWrongContentType(): void
@@ -371,11 +373,31 @@ public function testDispatchPostWithWrongContentType(): void
371373
$this->request->setMethod('POST');
372374
$this->request->setContent(json_encode($postData));
373375
$response = $this->graphql->dispatch($this->request);
374-
self::assertEquals(400, $response->getStatusCode());
376+
self::assertEquals(415, $response->getStatusCode());
375377
$output = $this->jsonSerializer->unserialize($response->getContent());
376378
self::assertArrayHasKey('errors', $output);
377379
self::assertNotEmpty($output['errors']);
378380
self::assertArrayHasKey('message', $output['errors'][0]);
379381
self::assertEquals('Request content type must be application/json', $output['errors'][0]['message']);
380382
}
383+
384+
public function testDispatchGetWithMutation(): void
385+
{
386+
$query = <<<QUERY
387+
mutation {
388+
createEmptyCart
389+
}
390+
QUERY;
391+
392+
$this->request->setPathInfo('/graphql');
393+
$this->request->setMethod('GET');
394+
$this->request->setQueryValue('query', $query);
395+
$response = $this->graphql->dispatch($this->request);
396+
self::assertEquals(405, $response->getStatusCode());
397+
$output = $this->jsonSerializer->unserialize($response->getContent());
398+
self::assertArrayHasKey('errors', $output);
399+
self::assertNotEmpty($output['errors']);
400+
self::assertArrayHasKey('message', $output['errors'][0]);
401+
self::assertEquals('Mutation requests allowed only for POST requests', $output['errors'][0]['message']);
402+
}
381403
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Framework\GraphQl\Exception;
9+
10+
use Throwable;
11+
12+
/**
13+
* Interface for providing response status code when invalid GraphQL request is detected.
14+
*/
15+
interface InvalidRequestInterface extends Throwable
16+
{
17+
/**
18+
* HTTP status code to be returned with the response.
19+
*
20+
* @return int
21+
*/
22+
public function getStatusCode(): int;
23+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Framework\GraphQl\Exception;
9+
10+
use GraphQL\Error\ClientAware;
11+
use Magento\Framework\Exception\LocalizedException;
12+
use Magento\Framework\Phrase;
13+
14+
class MethodNotAllowedException extends LocalizedException implements InvalidRequestInterface, ClientAware
15+
{
16+
/**
17+
* @param Phrase $phrase
18+
* @param \Exception|null $cause
19+
* @param int $code
20+
* @param bool $isSafe
21+
*/
22+
public function __construct(
23+
Phrase $phrase,
24+
?\Exception $cause = null,
25+
int $code = 0,
26+
private readonly bool $isSafe = true,
27+
) {
28+
parent::__construct($phrase, $cause, $code);
29+
}
30+
31+
/**
32+
* @inheritdoc
33+
*/
34+
public function getStatusCode(): int
35+
{
36+
return 405;
37+
}
38+
39+
/**
40+
* @inheritdoc
41+
*/
42+
public function isClientSafe(): bool
43+
{
44+
return $this->isSafe;
45+
}
46+
}

0 commit comments

Comments
 (0)