diff --git a/features/main/exception_to_status.feature b/features/main/exception_to_status.feature index d95a6112d4e..a182ea848a8 100644 --- a/features/main/exception_to_status.feature +++ b/features/main/exception_to_status.feature @@ -38,3 +38,10 @@ Feature: Using exception_to_status config And I send a "DELETE" request to "/error_with_overriden_status/1" Then the response status code should be 403 And the JSON node "status" should be equal to 403 + + @!mongodb + Scenario: Get HTTP Exception headers + When I add "Accept" header equal to "application/ld+json" + And I send a "GET" request to "/issue5924" + Then the response status code should be 429 + Then the header "retry-after" should be equal to 32 diff --git a/src/State/Processor/RespondProcessor.php b/src/State/Processor/RespondProcessor.php index 99e68360e03..b58f97239c0 100644 --- a/src/State/Processor/RespondProcessor.php +++ b/src/State/Processor/RespondProcessor.php @@ -13,6 +13,7 @@ namespace ApiPlatform\State\Processor; +use ApiPlatform\Metadata\Exception\HttpExceptionInterface; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operation; @@ -24,6 +25,7 @@ use ApiPlatform\Metadata\Util\CloneTrait; use ApiPlatform\State\ProcessorInterface; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface; /** * Serializes data. @@ -64,6 +66,11 @@ public function process(mixed $data, Operation $operation, array $uriVariables = 'X-Frame-Options' => 'deny', ]; + $exception = $request->attributes->get('exception'); + if (($exception instanceof HttpExceptionInterface || $exception instanceof SymfonyHttpExceptionInterface) && $exceptionHeaders = $exception->getHeaders()) { + $headers = array_merge($headers, $exceptionHeaders); + } + $status = $operation->getStatus(); if ($sunset = $operation->getSunset()) { diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue5924/TooManyRequests.php b/tests/Fixtures/TestBundle/ApiResource/Issue5924/TooManyRequests.php new file mode 100644 index 00000000000..757e282da32 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue5924/TooManyRequests.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5924; + +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; +use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; + +#[Get(uriTemplate: 'issue5924{._format}', read: true, provider: [TooManyRequests::class, 'provide'])] +class TooManyRequests +{ + public static function provide(Operation $operation, array $uriVariables = [], array $context = []): void + { + throw new TooManyRequestsHttpException(32); + } +} diff --git a/tests/State/RespondProcessorTest.php b/tests/State/RespondProcessorTest.php index 449215ab155..935d4109640 100644 --- a/tests/State/RespondProcessorTest.php +++ b/tests/State/RespondProcessorTest.php @@ -25,6 +25,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; class RespondProcessorTest extends TestCase { @@ -97,4 +98,19 @@ public function testRedirectToOperation(): void $this->assertSame(200, $response->getStatusCode()); $this->assertNull($response->headers->get('Location')); } + + public function testAddsExceptionHeaders(): void + { + $operation = new Get(); + + /** @var ProcessorInterface $respondProcessor */ + $respondProcessor = new RespondProcessor(); + $req = new Request(); + $req->attributes->set('exception', new TooManyRequestsHttpException(32)); + $response = $respondProcessor->process('content', new Get(), context: [ + 'request' => $req, + ]); + + $this->assertSame('32', $response->headers->get('retry-after')); + } }