Skip to content

Commit 03668fb

Browse files
committed
Adding GraphQL custom error format support
1 parent ac8ec40 commit 03668fb

File tree

19 files changed

+484
-18
lines changed

19 files changed

+484
-18
lines changed

features/graphql/mutation.feature

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,12 @@ Feature: GraphQL mutation support
332332
Then the response status code should be 200
333333
And the response should be in JSON
334334
And the header "Content-Type" should be equal to "application/json"
335-
And the JSON node "errors[0].message" should be equal to "name: This value should not be blank."
335+
And the JSON node "errors[0].status" should be equal to "400"
336+
And the JSON node "errors[0].message" should be equal to "Validation Exception"
337+
And the JSON node "errors[0].violations" should exist
338+
And the JSON node "errors[0].violations[0].path" should be equal to "name"
339+
And the JSON node "errors[0].violations[0].message" should be equal to "This value should not be blank."
340+
336341

337342
Scenario: Execute a custom mutation
338343
Given there are 1 dummyCustomMutation objects

src/Bridge/Symfony/Bundle/ApiPlatformBundle.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\DataProviderPass;
1818
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\ElasticsearchClientPass;
1919
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\FilterPass;
20+
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\GraphQlExceptionFormatterPass;
2021
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\GraphQlMutationResolverPass;
2122
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\GraphQlQueryResolverPass;
2223
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass;
2324
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass;
2425
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
25-
use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass;
2626
use Symfony\Component\DependencyInjection\ContainerBuilder;
2727
use Symfony\Component\HttpKernel\Bundle\Bundle;
2828

@@ -48,6 +48,7 @@ public function build(ContainerBuilder $container)
4848
$container->addCompilerPass(new GraphQlTypePass());
4949
$container->addCompilerPass(new GraphQlQueryResolverPass());
5050
$container->addCompilerPass(new GraphQlMutationResolverPass());
51+
$container->addCompilerPass(new GraphQlExceptionFormatterPass());
5152
$container->addCompilerPass(new MetadataAwareNameConverterPass());
5253
}
5354
}

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
2929
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
3030
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
31+
use ApiPlatform\Core\GraphQl\Exception\ExceptionFormatterInterface;
3132
use ApiPlatform\Core\GraphQl\Resolver\MutationResolverInterface;
3233
use ApiPlatform\Core\GraphQl\Resolver\QueryCollectionResolverInterface;
3334
use ApiPlatform\Core\GraphQl\Resolver\QueryItemResolverInterface;
@@ -395,6 +396,8 @@ private function registerGraphQlConfiguration(ContainerBuilder $container, array
395396
->addTag('api_platform.graphql.mutation_resolver');
396397
$container->registerForAutoconfiguration(GraphQlTypeInterface::class)
397398
->addTag('api_platform.graphql.type');
399+
$container->registerForAutoconfiguration(ExceptionFormatterInterface::class)
400+
->addTag('api_platform.graphql.exception_formatter');
398401
}
399402

400403
private function registerLegacyBundlesConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler;
15+
16+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17+
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
use Symfony\Component\DependencyInjection\Reference;
19+
20+
/**
21+
* Injects GraphQL Exception formatters.
22+
*
23+
* @internal
24+
*
25+
* @author Mahmood Bazdar <[email protected]>
26+
*/
27+
class GraphQlExceptionFormatterPass implements CompilerPassInterface
28+
{
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function process(ContainerBuilder $container)
33+
{
34+
if (!$container->getParameter('api_platform.graphql.enabled')) {
35+
return;
36+
}
37+
38+
$formatters = [];
39+
foreach ($container->findTaggedServiceIds('api_platform.graphql.exception_formatter', true) as $serviceId => $tags) {
40+
foreach ($tags as $tag) {
41+
$formatters[$tag['id'] ?? $serviceId] = new Reference($serviceId);
42+
}
43+
}
44+
$container->getDefinition('api_platform.graphql.exception_formatter_locator')->addArgument($formatters);
45+
$container->getDefinition('api_platform.graphql.exception_formatter_factory')->addArgument(array_keys($formatters));
46+
}
47+
}

src/Bridge/Symfony/Bundle/Resources/config/graphql.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,26 @@
152152
<argument type="service" id="api_platform.graphql.types_container" />
153153
<argument type="service" id="api_platform.graphql.fields_builder" />
154154
</service>
155+
<!-- Exception formatter -->
156+
<service id="api_platform.graphql.validation_exception_formatter" class="ApiPlatform\Core\GraphQl\Exception\Formatter\ValidationExceptionFormatter">
157+
<tag name="api_platform.graphql.exception_formatter" />
158+
</service>
155159

160+
<service id="api_platform.graphql.exception_formatter_locator" class="Symfony\Component\DependencyInjection\ServiceLocator">
161+
<tag name="container.service_locator" />
162+
</service>
163+
164+
<service id="api_platform.graphql.exception_formatter_factory" class="ApiPlatform\Core\GraphQl\Exception\ExceptionFormatterFactory">
165+
<argument type="service" id="api_platform.graphql.exception_formatter_locator" />
166+
</service>
156167
<!-- Action -->
157168

158169
<service id="api_platform.graphql.action.entrypoint" class="ApiPlatform\Core\GraphQl\Action\EntrypointAction" public="true">
159170
<argument type="service" id="api_platform.graphql.schema_builder" />
160171
<argument type="service" id="api_platform.graphql.executor" />
161172
<argument type="service" id="api_platform.graphql.action.graphiql" />
162173
<argument type="service" id="api_platform.graphql.action.graphql_playground" />
174+
<argument type="service" id="api_platform.graphql.exception_formatter_factory" />
163175
<argument>%kernel.debug%</argument>
164176
<argument>%api_platform.graphql.graphiql.enabled%</argument>
165177
<argument>%api_platform.graphql.graphql_playground.enabled%</argument>

src/GraphQl/Action/EntrypointAction.php

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313

1414
namespace ApiPlatform\Core\GraphQl\Action;
1515

16+
use ApiPlatform\Core\GraphQl\Exception\ExceptionFormatterFactory;
17+
use ApiPlatform\Core\GraphQl\Exception\ExceptionFormatterInterface;
1618
use ApiPlatform\Core\GraphQl\ExecutorInterface;
1719
use ApiPlatform\Core\GraphQl\Type\SchemaBuilderInterface;
1820
use GraphQL\Error\Debug;
1921
use GraphQL\Error\Error;
22+
use GraphQL\Error\FormattedError;
2023
use GraphQL\Executor\ExecutionResult;
2124
use Symfony\Component\HttpFoundation\JsonResponse;
2225
use Symfony\Component\HttpFoundation\Request;
@@ -37,8 +40,9 @@ final class EntrypointAction
3740
private $graphiqlEnabled;
3841
private $graphQlPlaygroundEnabled;
3942
private $defaultIde;
43+
private $exceptionFormatterFactory;
4044

41-
public function __construct(SchemaBuilderInterface $schemaBuilder, ExecutorInterface $executor, GraphiQlAction $graphiQlAction, GraphQlPlaygroundAction $graphQlPlaygroundAction, bool $debug = false, bool $graphiqlEnabled = false, bool $graphQlPlaygroundEnabled = false, $defaultIde = false)
45+
public function __construct(SchemaBuilderInterface $schemaBuilder, ExecutorInterface $executor, GraphiQlAction $graphiQlAction, GraphQlPlaygroundAction $graphQlPlaygroundAction, ExceptionFormatterFactory $exceptionFormatterFactory, bool $debug = false, bool $graphiqlEnabled = false, bool $graphQlPlaygroundEnabled = false, $defaultIde = false)
4246
{
4347
$this->schemaBuilder = $schemaBuilder;
4448
$this->executor = $executor;
@@ -48,6 +52,7 @@ public function __construct(SchemaBuilderInterface $schemaBuilder, ExecutorInter
4852
$this->graphiqlEnabled = $graphiqlEnabled;
4953
$this->graphQlPlaygroundEnabled = $graphQlPlaygroundEnabled;
5054
$this->defaultIde = $defaultIde;
55+
$this->exceptionFormatterFactory = $exceptionFormatterFactory;
5156
}
5257

5358
public function __invoke(Request $request): Response
@@ -73,7 +78,30 @@ public function __invoke(Request $request): Response
7378
}
7479

7580
try {
76-
$executionResult = $this->executor->executeQuery($this->schemaBuilder->getSchema(), $query, null, null, $variables, $operation);
81+
$executionResult = $this->executor->executeQuery($this->schemaBuilder->getSchema(), $query, null, null, $variables, $operation)
82+
->setErrorFormatter(function (Error $error) {
83+
$formatters = $this->exceptionFormatterFactory->getExceptionFormatters();
84+
usort($formatters, function ($a, $b) {
85+
/**
86+
* @var ExceptionFormatterInterface
87+
* @var ExceptionFormatterInterface $b
88+
*/
89+
if ($a->getPriority() == $b->getPriority()) {
90+
return 0;
91+
}
92+
93+
return ($a->getPriority() > $b->getPriority()) ? -1 : 1;
94+
});
95+
/** @var ExceptionFormatterInterface $exceptionFormatter */
96+
foreach ($formatters as $exceptionFormatter) {
97+
if (null !== $error->getPrevious() && $exceptionFormatter->supports($error->getPrevious())) {
98+
return $exceptionFormatter->format($error);
99+
}
100+
}
101+
102+
// falling back to default GraphQL error formatter
103+
return FormattedError::createFromException($error);
104+
});
77105
} catch (\Exception $e) {
78106
$executionResult = new ExecutionResult(null, [new Error($e->getMessage(), null, null, null, null, $e)]);
79107
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\GraphQl\Exception;
15+
16+
use ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface;
17+
use Psr\Container\ContainerInterface;
18+
19+
/**
20+
* Get the registered services corresponding to GraphQL exception formatters.
21+
*
22+
* @author Mahmood Bazdar <[email protected]>
23+
*/
24+
class ExceptionFormatterFactory implements ExceptionFormatterFactoryInterface
25+
{
26+
private $exceptionFormatterLocator;
27+
private $exceptionFormatterIds;
28+
29+
/**
30+
* @param string[] $exceptionFormatterIds
31+
*/
32+
public function __construct(ContainerInterface $exceptionFormatterLocator, array $exceptionFormatterIds)
33+
{
34+
$this->exceptionFormatterLocator = $exceptionFormatterLocator;
35+
$this->exceptionFormatterIds = $exceptionFormatterIds;
36+
}
37+
38+
public function getExceptionFormatters(): array
39+
{
40+
$exceptionFormatters = [];
41+
42+
foreach ($this->exceptionFormatterIds as $exceptionFormatterId) {
43+
/** @var TypeInterface $exceptionFormatter */
44+
$exceptionFormatter = $this->exceptionFormatterLocator->get($exceptionFormatterId);
45+
$exceptionFormatters[$exceptionFormatterId] = $exceptionFormatter;
46+
}
47+
48+
return $exceptionFormatters;
49+
}
50+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\GraphQl\Exception;
15+
16+
/**
17+
* Get Exception formatters.
18+
*
19+
* @author Mahmood Bazdar <[email protected]>
20+
*/
21+
interface ExceptionFormatterFactoryInterface
22+
{
23+
public function getExceptionFormatters(): array;
24+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\GraphQl\Exception;
15+
16+
use GraphQL\Error\Error;
17+
18+
/**
19+
* @expremintal
20+
*
21+
* @author Mahmood Bazdar <[email protected]>
22+
*/
23+
interface ExceptionFormatterInterface
24+
{
25+
/**
26+
* Formats the exception and returns the formatted array.
27+
*/
28+
public function format(Error $error): array;
29+
30+
/**
31+
* Check the exception, return true if you can format the exception.
32+
*/
33+
public function supports(\Throwable $exception): bool;
34+
35+
/**
36+
* Priority of your formatter in container. Higher number will be called sooner.
37+
*/
38+
public function getPriority(): int;
39+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\GraphQl\Exception\Formatter;
15+
16+
use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
17+
use ApiPlatform\Core\GraphQl\Exception\ExceptionFormatterInterface;
18+
use GraphQL\Error\Error;
19+
use GraphQL\Error\FormattedError;
20+
use Symfony\Component\HttpFoundation\Response;
21+
use Symfony\Component\Validator\ConstraintViolation;
22+
23+
/**
24+
* Formats Validation exception.
25+
*
26+
* @author Mahmood Bazdar <[email protected]>
27+
*/
28+
class ValidationExceptionFormatter implements ExceptionFormatterInterface
29+
{
30+
/**
31+
* {@inheritdoc}
32+
*/
33+
public function format(Error $error): array
34+
{
35+
/**
36+
* @var ValidationException
37+
*/
38+
$validationException = $error->getPrevious();
39+
$error = FormattedError::createFromException($error);
40+
$error['message'] = $validationException->getMessage();
41+
$error['status'] = Response::HTTP_BAD_REQUEST;
42+
$error['extensions']['category'] = Error::CATEGORY_GRAPHQL;
43+
$error['violations'] = [];
44+
45+
/** @var ConstraintViolation $violation */
46+
foreach ($validationException->getConstraintViolationList() as $violation) {
47+
$error['violations'][] = [
48+
'path' => $violation->getPropertyPath(),
49+
'message' => $violation->getMessage(),
50+
];
51+
}
52+
53+
return $error;
54+
}
55+
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public function supports(\Throwable $exception): bool
60+
{
61+
return $exception instanceof ValidationException;
62+
}
63+
64+
/**
65+
* {@inheritdoc}
66+
*/
67+
public function getPriority(): int
68+
{
69+
return -100;
70+
}
71+
}

0 commit comments

Comments
 (0)