Skip to content

Commit 5b2cfdc

Browse files
authored
[ValidationException] Allow customization of validation error status code (#3808)
* feat: support changing validation status code * fix: apply phpcs fixer * fix: ValidationExceptionListener - default to 422 status * fix: grafql default to 422 status * test: update behat features * test: update phpunit tests
1 parent 3cf90f7 commit 5b2cfdc

File tree

12 files changed

+46
-13
lines changed

12 files changed

+46
-13
lines changed

features/graphql/mutation.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ Feature: GraphQL mutation support
674674
Then the response status code should be 200
675675
And the response should be in JSON
676676
And the header "Content-Type" should be equal to "application/json"
677-
And the JSON node "errors[0].extensions.status" should be equal to "400"
677+
And the JSON node "errors[0].extensions.status" should be equal to "422"
678678
And the JSON node "errors[0].message" should be equal to "name: This value should not be blank."
679679
And the JSON node "errors[0].extensions.violations" should exist
680680
And the JSON node "errors[0].extensions.violations[0].path" should be equal to "name"

features/hal/problem.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Feature: Error handling valid according to RFC 7807 (application/problem+json)
1010
"""
1111
{}
1212
"""
13-
Then the response status code should be 400
13+
Then the response status code should be 422
1414
And the response should be in JSON
1515
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
1616
And the JSON should be equal to:

features/hydra/error.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Feature: Error handling
99
"""
1010
{}
1111
"""
12-
Then the response status code should be 400
12+
Then the response status code should be 422
1313
And the response should be in JSON
1414
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
1515
And the JSON should be equal to:

features/jsonapi/errors.feature

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Feature: JSON API error handling
1818
}
1919
}
2020
"""
21-
Then the response status code should be 400
21+
Then the response status code should be 422
2222
And the response should be in JSON
2323
And the JSON should be valid according to the JSON API schema
2424
And the JSON should be equal to:
@@ -49,7 +49,7 @@ Feature: JSON API error handling
4949
}
5050
}
5151
"""
52-
Then the response status code should be 400
52+
Then the response status code should be 422
5353
And the response should be in JSON
5454
And the JSON should be valid according to the JSON API schema
5555
And the JSON should be equal to:

features/main/validation.feature

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Feature: Using validations groups
2424
"code": "My Dummy"
2525
}
2626
"""
27-
Then the response status code should be 400
27+
Then the response status code should be 422
2828
And the response should be in JSON
2929
And the JSON should be equal to:
3030
"""
@@ -52,7 +52,7 @@ Feature: Using validations groups
5252
"code": "My Dummy"
5353
}
5454
"""
55-
Then the response status code should be 400
55+
Then the response status code should be 422
5656
And the response should be in JSON
5757
And the JSON should be equal to:
5858
"""

features/security/send_security_headers.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Feature: Send security header
2727
"""
2828
{"name": ""}
2929
"""
30-
Then the response status code should be 400
30+
Then the response status code should be 422
3131
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
3232
And the header "X-Content-Type-Options" should be equal to "nosniff"
3333
And the header "X-Frame-Options" should be equal to "deny"

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@
202202
<service id="api_platform.listener.exception.validation" class="ApiPlatform\Core\Bridge\Symfony\Validator\EventListener\ValidationExceptionListener">
203203
<argument type="service" id="api_platform.serializer" />
204204
<argument>%api_platform.error_formats%</argument>
205+
<argument>%api_platform.exception_to_status%</argument>
205206

206207
<tag name="kernel.event_listener" event="kernel.exception" method="onKernelException" />
207208
</service>

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@
239239
</service>
240240

241241
<service id="api_platform.graphql.normalizer.validation_exception" class="ApiPlatform\Core\GraphQl\Serializer\Exception\ValidationExceptionNormalizer">
242+
<argument>%api_platform.exception_to_status%</argument>
243+
242244
<tag name="serializer.normalizer" priority="-780" />
243245
</service>
244246

src/Bridge/Symfony/Validator/EventListener/ValidationExceptionListener.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ final class ValidationExceptionListener
2828
{
2929
private $serializer;
3030
private $errorFormats;
31+
private $exceptionToStatus;
3132

32-
public function __construct(SerializerInterface $serializer, array $errorFormats)
33+
public function __construct(SerializerInterface $serializer, array $errorFormats, array $exceptionToStatus = [])
3334
{
3435
$this->serializer = $serializer;
3536
$this->errorFormats = $errorFormats;
37+
$this->exceptionToStatus = $exceptionToStatus;
3638
}
3739

3840
/**
@@ -44,12 +46,22 @@ public function onKernelException(ExceptionEvent $event): void
4446
if (!$exception instanceof ValidationException) {
4547
return;
4648
}
49+
$exceptionClass = \get_class($exception);
50+
$statusCode = Response::HTTP_UNPROCESSABLE_ENTITY;
51+
52+
foreach ($this->exceptionToStatus as $class => $status) {
53+
if (is_a($exceptionClass, $class, true)) {
54+
$statusCode = $status;
55+
56+
break;
57+
}
58+
}
4759

4860
$format = ErrorFormatGuesser::guessErrorFormat($event->getRequest(), $this->errorFormats);
4961

5062
$event->setResponse(new Response(
5163
$this->serializer->serialize($exception->getConstraintViolationList(), $format['key']),
52-
Response::HTTP_BAD_REQUEST,
64+
$statusCode,
5365
[
5466
'Content-Type' => sprintf('%s; charset=utf-8', $format['value'][0]),
5567
'X-Content-Type-Options' => 'nosniff',

src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@
3030
*/
3131
final class ValidationExceptionNormalizer implements NormalizerInterface
3232
{
33+
private $exceptionToStatus;
34+
35+
public function __construct(array $exceptionToStatus = [])
36+
{
37+
$this->exceptionToStatus = $exceptionToStatus;
38+
}
39+
3340
/**
3441
* {@inheritdoc}
3542
*/
@@ -39,7 +46,18 @@ public function normalize($object, $format = null, array $context = []): array
3946
$validationException = $object->getPrevious();
4047
$error = FormattedError::createFromException($object);
4148
$error['message'] = $validationException->getMessage();
42-
$error['extensions']['status'] = Response::HTTP_BAD_REQUEST;
49+
50+
$exceptionClass = \get_class($validationException);
51+
$statusCode = Response::HTTP_UNPROCESSABLE_ENTITY;
52+
53+
foreach ($this->exceptionToStatus as $class => $status) {
54+
if (is_a($exceptionClass, $class, true)) {
55+
$statusCode = $status;
56+
57+
break;
58+
}
59+
}
60+
$error['extensions']['status'] = $statusCode;
4361
$error['extensions']['category'] = 'user';
4462
$error['extensions']['violations'] = [];
4563

tests/Bridge/Symfony/Validator/EventListener/ValidationExceptionListenerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public function testValidationException()
5757
$response = $event->getResponse();
5858
$this->assertInstanceOf(Response::class, $response);
5959
$this->assertSame($exceptionJson, $response->getContent());
60-
$this->assertSame(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
60+
$this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response->getStatusCode());
6161
$this->assertSame('application/ld+json; charset=utf-8', $response->headers->get('Content-Type'));
6262
$this->assertSame('nosniff', $response->headers->get('X-Content-Type-Options'));
6363
$this->assertSame('deny', $response->headers->get('X-Frame-Options'));

tests/GraphQl/Serializer/Exception/ValidationExceptionNormalizerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public function testNormalize(): void
4848

4949
$normalizedError = $this->validationExceptionNormalizer->normalize($error);
5050
$this->assertSame($exceptionMessage, $normalizedError['message']);
51-
$this->assertSame(400, $normalizedError['extensions']['status']);
51+
$this->assertSame(422, $normalizedError['extensions']['status']);
5252
$this->assertSame('user', $normalizedError['extensions']['category']);
5353
$this->assertArrayHasKey('violations', $normalizedError['extensions']);
5454
$this->assertSame([

0 commit comments

Comments
 (0)