Skip to content

Commit 6dd6608

Browse files
committed
ACP2E-4194: GraphQL Exception in SWAT
1 parent 15c5cee commit 6dd6608

File tree

9 files changed

+184
-46
lines changed

9 files changed

+184
-46
lines changed

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException;
2525
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
2626
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
27+
use Magento\Framework\GraphQl\Exception\InvalidRequestInterface;
2728
use Magento\Framework\GraphQl\Query\Fields as QueryFields;
2829
use Magento\Framework\GraphQl\Query\QueryParser;
2930
use Magento\Framework\GraphQl\Query\QueryProcessor;
@@ -34,7 +35,6 @@
3435
use Magento\GraphQl\Helper\Query\Logger\LogData;
3536
use Magento\GraphQl\Model\Query\ContextFactoryInterface;
3637
use Magento\GraphQl\Model\Query\Logger\LoggerPool;
37-
use Throwable;
3838

3939
/**
4040
* Front controller for web API GraphQL area.
@@ -242,15 +242,15 @@ public function dispatch(RequestInterface $request): ResponseInterface
242242
*
243243
* @param Exception $e
244244
* @return array
245-
* @throws Throwable
246245
*/
247246
private function handleGraphQlException(Exception $e): array
248247
{
249248
[$error, $statusCode] = match (true) {
250-
$e instanceof GraphQlInputException => [FormattedError::createFromException($e), 200],
249+
$e instanceof InvalidRequestInterface => [FormattedError::createFromException($e), $e->getStatusCode()],
251250
$e instanceof SyntaxError => [FormattedError::createFromException($e), 400],
252251
$e instanceof GraphQlAuthenticationException => [$this->graphQlError->create($e), 401],
253252
$e instanceof GraphQlAuthorizationException => [$this->graphQlError->create($e), 403],
253+
$e instanceof GraphQlInputException => [FormattedError::createFromException($e), 200],
254254
default => [$this->graphQlError->create($e), ExceptionFormatter::HTTP_GRAPH_QL_SCHEMA_ERROR_STATUS],
255255
};
256256

@@ -293,19 +293,19 @@ private function getDataFromRequest(RequestInterface $request): array
293293
$content = $request->getContent();
294294
try {
295295
$data = $this->jsonSerializer->unserialize($content);
296-
} catch (\InvalidArgumentException $e) {
296+
} catch (\InvalidArgumentException) {
297297
$source = new Source($content);
298-
throw new SyntaxError($source, 0, $e->getMessage());
298+
throw new SyntaxError($source, 0, 'Unable to parse the request.');
299299
}
300300
} elseif ($request->isGet()) {
301301
$data = $request->getParams();
302302
try {
303303
$data['variables'] = !empty($data['variables']) && is_string($data['variables'])
304304
? $this->jsonSerializer->unserialize($data['variables'])
305305
: null;
306-
} catch (\InvalidArgumentException $e) {
306+
} catch (\InvalidArgumentException) {
307307
$source = new Source($data['variables']);
308-
throw new SyntaxError($source, 0, $e->getMessage());
308+
throw new SyntaxError($source, 0, 'Unable to parse the variables.');
309309
}
310310
}
311311

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
}

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+
}
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 UnsupportedMediaTypeException 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 415;
37+
}
38+
39+
/**
40+
* @inheritdoc
41+
*/
42+
public function isClientSafe(): bool
43+
{
44+
return $this->isSafe;
45+
}
46+
}

lib/internal/Magento/Framework/GraphQl/Schema/SchemaGenerator.php

Lines changed: 2 additions & 2 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 2018 Adobe
4+
* All Rights Reserved.
55
*/
66
declare(strict_types=1);
77

lib/internal/Magento/Framework/GraphQl/Schema/Type/TypeRegistry.php

Lines changed: 2 additions & 2 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

0 commit comments

Comments
 (0)