diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 8d3859ff905b8..630eb53ad8bb4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add native return type to `Translator` and to `Application::reset()` + * Allow customizing HTTP status code on validation error occurred in `#[MapRequestPayload]` 6.3 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 708b43b840c84..4d32013228d4b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -139,6 +139,15 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue('error_controller') ->end() ->booleanNode('handle_all_throwables')->info('HttpKernel will handle all kinds of \Throwable')->end() + ->arrayNode('map_request_payload') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('status_code_on_error') + ->info('HTTP status code to be returned on error occurred while parsing the request payload.') + ->defaultNull() + ->end() + ->end() + ->end() ->end() ; diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 56a4363aba5e5..5964e0f70a2c6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -280,6 +280,7 @@ public function load(array $configs, ContainerBuilder $container) $container->getDefinition('locale_listener')->replaceArgument(3, $config['set_locale_from_accept_language']); $container->getDefinition('response_listener')->replaceArgument(1, $config['set_content_language_from_locale']); $container->getDefinition('http_kernel')->replaceArgument(4, $config['handle_all_throwables'] ?? false); + $container->getDefinition('argument_resolver.request_payload')->replaceArgument(3, $config['map_request_payload']['status_code_on_error']); // If the slugger is used but the String component is not available, we should throw an error if (!ContainerBuilder::willBeAvailable('symfony/string', SluggerInterface::class, ['symfony/framework-bundle'])) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php index a3a6ef7735612..cfeddc4bfee00 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php @@ -68,6 +68,7 @@ service('serializer'), service('validator')->nullOnInvalid(), service('translator')->nullOnInvalid(), + abstract_arg('The "map_request_payload.status_code_on_error" config value'), ]) ->tag('controller.targeted_value_resolver', ['name' => RequestPayloadValueResolver::class]) ->tag('kernel.event_subscriber') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index e3ecd982d3cc8..07222269c0891 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -739,6 +739,9 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'remote-event' => [ 'enabled' => false, ], + 'map_request_payload' => [ + 'status_code_on_error' => null, + ], ]; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ApiAttributesTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ApiAttributesTest.php index 96b6d0ee98e14..fbd7f43bde350 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ApiAttributesTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ApiAttributesTest.php @@ -349,6 +349,24 @@ public static function mapRequestPayloadProvider(): iterable 'expectedStatusCode' => 422, ]; } + + public function testMapRequestPayloadError400() + { + $client = self::createClient(['test_case' => 'ApiAttributesTest']); + + $client->request( + 'POST', + '/map-request-body-error-400', + [], + [], + ['HTTP_ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json'], + json_encode([]), + ); + + $response = $client->getResponse(); + + self::assertSame(Response::HTTP_BAD_REQUEST, $response->getStatusCode()); + } } class WithMapQueryStringController @@ -388,6 +406,16 @@ public function __invoke(#[MapRequestPayload] ?RequestBody $body, Request $reque } } +class WithMapRequestPayloadError400Controller +{ + public function __invoke( + #[MapRequestPayload(statusCodeOnError: Response::HTTP_BAD_REQUEST)] ?RequestBody $body, + Request $request, + ): Response { + return new Response('', Response::HTTP_NO_CONTENT); + } +} + class QueryString { public function __construct( diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ApiAttributesTest/routing.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ApiAttributesTest/routing.yml index 9ec40e1708c2b..72689814baa9f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ApiAttributesTest/routing.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ApiAttributesTest/routing.yml @@ -5,3 +5,7 @@ map_query_string: map_request_body: path: /map-request-body.{_format} controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapRequestPayloadController + +map_request_body_error_400: + path: /map-request-body-error-400 + controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapRequestPayloadError400Controller diff --git a/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php b/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php index 02c01fa40bf39..390cf7aac6eaf 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php +++ b/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php @@ -30,6 +30,7 @@ public function __construct( public readonly array $serializationContext = [], public readonly string|GroupSequence|array|null $validationGroups = null, string $resolver = RequestPayloadValueResolver::class, + public readonly ?int $statusCodeOnError = null, ) { parent::__construct($resolver); } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php index 370097cda4b08..303c3a8c35e82 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php @@ -59,6 +59,7 @@ public function __construct( private readonly SerializerInterface&DenormalizerInterface $serializer, private readonly ?ValidatorInterface $validator = null, private readonly ?TranslatorInterface $translator = null, + private readonly ?int $statusCodeOnError = null, ) { } @@ -91,7 +92,9 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo $validationFailedCode = Response::HTTP_NOT_FOUND; } elseif ($argument instanceof MapRequestPayload) { $payloadMapper = 'mapRequestPayload'; - $validationFailedCode = Response::HTTP_UNPROCESSABLE_ENTITY; + $validationFailedCode = $argument->statusCodeOnError + ?? $this->statusCodeOnError + ?? Response::HTTP_UNPROCESSABLE_ENTITY; } else { continue; }