Skip to content

Commit

Permalink
Allow defining exception_to_status per operation
Browse files Browse the repository at this point in the history
  • Loading branch information
julienfalque committed Apr 24, 2020
1 parent f52a17e commit 0c89695
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 2 deletions.
39 changes: 37 additions & 2 deletions src/Action/ExceptionAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

namespace ApiPlatform\Core\Action;

use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Util\ErrorFormatGuesser;
use ApiPlatform\Core\Util\RequestAttributesExtractor;
use Symfony\Component\Debug\Exception\FlattenException as LegacyFlattenException;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
Expand All @@ -32,15 +34,21 @@ final class ExceptionAction
private $errorFormats;
private $exceptionToStatus;

/**
* @var ResourceMetadataFactoryInterface|null
*/
private $resourceMetadataFactory;

/**
* @param array $errorFormats A list of enabled error formats
* @param array $exceptionToStatus A list of exceptions mapped to their HTTP status code
*/
public function __construct(SerializerInterface $serializer, array $errorFormats, array $exceptionToStatus = [])
public function __construct(SerializerInterface $serializer, array $errorFormats, array $exceptionToStatus = [], ?ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
{
$this->serializer = $serializer;
$this->errorFormats = $errorFormats;
$this->exceptionToStatus = $exceptionToStatus;
$this->resourceMetadataFactory = $resourceMetadataFactory;
}

/**
Expand All @@ -53,7 +61,12 @@ public function __invoke($exception, Request $request): Response
$exceptionClass = $exception->getClass();
$statusCode = $exception->getStatusCode();

foreach ($this->exceptionToStatus as $class => $status) {
$exceptionToStatus = array_merge(
$this->exceptionToStatus,
$this->getOperationExceptionToStatus($request)
);

foreach ($exceptionToStatus as $class => $status) {
if (is_a($exceptionClass, $class, true)) {
$statusCode = $status;

Expand All @@ -69,4 +82,26 @@ public function __invoke($exception, Request $request): Response

return new Response($this->serializer->serialize($exception, $format['key'], ['statusCode' => $statusCode]), $statusCode, $headers);
}

private function getOperationExceptionToStatus(Request $request): array
{
$attributes = RequestAttributesExtractor::extractAttributes($request);

if (null === $this->resourceMetadataFactory || null === $attributes['resource_class']) {
return [];
}

$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);

$exceptionToStatus = $resourceMetadata->getOperationAttribute($attributes, 'exception_to_status', [], false);

if (!\is_array($exceptionToStatus)) {
throw new \LogicException();
}

return array_merge(
$resourceMetadata->getAttribute('exception_to_status', []),
$exceptionToStatus
);
}
}
1 change: 1 addition & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/api.xml
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@
<argument type="service" id="api_platform.serializer" />
<argument>%api_platform.error_formats%</argument>
<argument>%api_platform.exception_to_status%</argument>
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
</service>

<!-- Identifiers -->
Expand Down
150 changes: 150 additions & 0 deletions tests/Action/ExceptionActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

use ApiPlatform\Core\Action\ExceptionAction;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use DomainException as DomainException;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
Expand Down Expand Up @@ -54,6 +57,153 @@ public function testActionWithCatchableException()
$this->assertTrue($response->headers->contains('X-Frame-Options', 'deny'));
}

/**
* @dataProvider provideOperationExceptionToStatusCases
*/
public function testActionWithOperationExceptionToStatus(
array $globalExceptionToStatus,
?array $resourceExceptionToStatus,
?array $operationExceptionToStatus,
int $expectedStatusCode
) {
$exception = new DomainException();
$flattenException = FlattenException::create($exception);

$serializer = $this->prophesize(SerializerInterface::class);
$serializer->serialize($flattenException, 'jsonproblem', ['statusCode' => $expectedStatusCode])->willReturn();

$resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class);
$resourceMetadataFactory->create('Foo')->willReturn(new ResourceMetadata(
'Foo',
null,
null,
[
'operation' => null !== $operationExceptionToStatus ? ['exception_to_status' => $operationExceptionToStatus] : [],
],
null,
null !== $resourceExceptionToStatus ? ['exception_to_status' => $resourceExceptionToStatus] : []
));

$exceptionAction = new ExceptionAction(
$serializer->reveal(),
[
'jsonproblem' => ['application/problem+json'],
'jsonld' => ['application/ld+json'],
],
$globalExceptionToStatus,
$resourceMetadataFactory->reveal()
);

$request = new Request();
$request->setFormat('jsonproblem', 'application/problem+json');
$request->attributes->replace([
'_api_resource_class' => 'Foo',
'_api_item_operation_name' => 'operation',
]);

$response = $exceptionAction($flattenException, $request);

$this->assertSame('', $response->getContent());
$this->assertSame($expectedStatusCode, $response->getStatusCode());
$this->assertTrue($response->headers->contains('Content-Type', 'application/problem+json; charset=utf-8'));
$this->assertTrue($response->headers->contains('X-Content-Type-Options', 'nosniff'));
$this->assertTrue($response->headers->contains('X-Frame-Options', 'deny'));
}

public function provideOperationExceptionToStatusCases()
{
yield 'no mapping' => [
[],
null,
null,
500,
];

yield 'on global attributes' => [
[DomainException::class => 100],
null,
null,
100,
];

yield 'on global attributes with empty resource and operation attributes' => [
[DomainException::class => 100],
[],
[],
100,
];

yield 'on global attributes and resource attributes' => [
[DomainException::class => 100],
[DomainException::class => 200],
null,
200,
];

yield 'on global attributes and resource attributes with empty operation attributes' => [
[DomainException::class => 100],
[DomainException::class => 200],
[],
200,
];

yield 'on global attributes and operation attributes' => [
[DomainException::class => 100],
null,
[DomainException::class => 300],
300,
];

yield 'on global attributes and operation attributes with empty resource attributes' => [
[DomainException::class => 100],
[],
[DomainException::class => 300],
300,
];

yield 'on global, resource and operation attributes' => [
[DomainException::class => 100],
[DomainException::class => 200],
[DomainException::class => 300],
300,
];

yield 'on resource attributes' => [
[],
[DomainException::class => 200],
null,
200,
];

yield 'on resource attributes with empty operation attributes' => [
[],
[DomainException::class => 200],
[],
200,
];

yield 'on resource and operation attributes' => [
[],
[DomainException::class => 200],
[DomainException::class => 300],
300,
];

yield 'on operation attributes' => [
[],
null,
[DomainException::class => 300],
300,
];

yield 'on operation attributes with empty resource attributes' => [
[],
[],
[DomainException::class => 300],
300,
];
}

public function testActionWithUncatchableException()
{
$serializerException = $this->prophesize(ExceptionInterface::class);
Expand Down

0 comments on commit 0c89695

Please sign in to comment.