diff --git a/src/JsonApi/Serializer/ErrorNormalizer.php b/src/JsonApi/Serializer/ErrorNormalizer.php index 2076a1c1d8..098ca8931d 100644 --- a/src/JsonApi/Serializer/ErrorNormalizer.php +++ b/src/JsonApi/Serializer/ErrorNormalizer.php @@ -25,7 +25,7 @@ final class ErrorNormalizer implements NormalizerInterface { public const FORMAT = 'jsonapi'; - public function __construct(private ?NormalizerInterface $itemNormalizer = null) + public function __construct(private ?NormalizerInterface $itemNormalizer = null, private ?NormalizerInterface $problemNormalizer = null) { } @@ -34,9 +34,24 @@ public function __construct(private ?NormalizerInterface $itemNormalizer = null) */ public function normalize(mixed $data, ?string $format = null, array $context = []): array { - $jsonApiObject = $this->itemNormalizer->normalize($data, $format, $context); - $error = $jsonApiObject['data']['attributes'] ?? []; - $error['id'] = $jsonApiObject['data']['id']; + if ($data instanceof FlattenException) { + $type = \sprintf('/errors/%d', $data->getStatusCode()); + unset($context['exception']); + $problem = $this->problemNormalizer->normalize($data, $format, ['type' => $type] + $context); + $error = [ + 'id' => $problem['type'], + 'title' => $problem['title'], + 'detail' => $problem['detail'], + 'status' => $problem['status'], + 'type' => $problem['type'], + 'code' => (string) $problem['status'], + ]; + } else { + $jsonApiObject = $this->itemNormalizer->normalize($data, $format, $context); + $error = $jsonApiObject['data']['attributes'] ?? []; + $error['id'] = $jsonApiObject['data']['id']; + } + if (isset($error['type'])) { $error['links'] = ['type' => $error['type']]; } @@ -76,7 +91,7 @@ public function normalize(mixed $data, ?string $format = null, array $context = */ public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool { - return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException); + return self::FORMAT === $format && ($data instanceof \Exception || ($data instanceof FlattenException && null !== $this->problemNormalizer)); } /** @@ -87,7 +102,7 @@ public function getSupportedTypes(?string $format): array if (self::FORMAT === $format) { return [ \Exception::class => true, - FlattenException::class => true, + FlattenException::class => null !== $this->problemNormalizer, ]; } diff --git a/src/Symfony/Bundle/Resources/config/jsonapi.php b/src/Symfony/Bundle/Resources/config/jsonapi.php index 4e6f610c44..b2ae1f2845 100644 --- a/src/Symfony/Bundle/Resources/config/jsonapi.php +++ b/src/Symfony/Bundle/Resources/config/jsonapi.php @@ -119,6 +119,6 @@ ->tag('serializer.normalizer', ['priority' => -780]); $services->set('api_platform.jsonapi.normalizer.error', ErrorNormalizer::class) - ->args([service('api_platform.jsonapi.normalizer.item')]) + ->args([service('api_platform.jsonapi.normalizer.item'), service('serializer.normalizer.problem')->nullOnInvalid()]) ->tag('serializer.normalizer', ['priority' => -790]); }; diff --git a/tests/Functional/JsonApi/ErrorTest.php b/tests/Functional/JsonApi/ErrorTest.php index 8833e5e73f..ca3b6a5962 100644 --- a/tests/Functional/JsonApi/ErrorTest.php +++ b/tests/Functional/JsonApi/ErrorTest.php @@ -17,6 +17,7 @@ use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonApi\ErrorProblem; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonApiErrorTestResource; use ApiPlatform\Tests\SetupClassResourcesTrait; +use Symfony\Contracts\HttpClient\ResponseInterface; class ErrorTest extends ApiTestCase { @@ -113,4 +114,47 @@ public function testNotFoundRouteRendersJsonApiFormat(): void $this->assertArrayHasKey('detail', $body['errors'][0]); $this->assertArrayHasKey('type', $body['errors'][0]); } + + public function testRouterNotFoundRendersJsonApiErrorObject(): void + { + $response = $this->requestWithSymfonyErrorHandling('GET', '/does_not_exist'); + + $this->assertResponseStatusCodeSame(404); + $this->assertResponseHeaderSame('content-type', 'application/vnd.api+json'); + $error = $response->toArray(false)['errors'][0]; + $this->assertSame('/errors/404', $error['id']); + $this->assertSame('An error occurred', $error['title']); + $this->assertStringContainsString('No route found', $error['detail']); + $this->assertSame('404', $error['status']); + $this->assertSame('404', $error['code']); + $this->assertSame('/errors/404', $error['type']); + $this->assertSame(['type' => '/errors/404'], $error['links']); + } + + public function testMethodNotAllowedRendersJsonApiErrorObject(): void + { + $response = $this->requestWithSymfonyErrorHandling('DELETE', '/jsonapi_error_test/existing'); + + $this->assertResponseStatusCodeSame(405); + $this->assertResponseHeaderSame('content-type', 'application/vnd.api+json'); + $error = $response->toArray(false)['errors'][0]; + $this->assertSame('/errors/405', $error['id']); + $this->assertSame('An error occurred', $error['title']); + $this->assertStringContainsString('Method Not Allowed', $error['detail']); + $this->assertSame('405', $error['status']); + $this->assertSame('405', $error['code']); + $this->assertSame('/errors/405', $error['type']); + $this->assertSame(['type' => '/errors/405'], $error['links']); + } + + private function requestWithSymfonyErrorHandling(string $method, string $uri): ResponseInterface + { + $client = self::createClient(); + $client->disableReboot(); + static::getContainer()->get('api_platform.listener.exception')->handleSymfonyErrors = false; + + return $client->request($method, $uri, [ + 'headers' => ['accept' => 'application/vnd.api+json'], + ]); + } }