Skip to content

Commit

Permalink
Add the ability to configure and match exceptions with an HTTP status…
Browse files Browse the repository at this point in the history
… code
  • Loading branch information
meyerbaptiste committed Oct 13, 2016
1 parent c1090d1 commit e9c1863
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 24 deletions.
30 changes: 21 additions & 9 deletions src/Action/ExceptionAction.php
Expand Up @@ -11,36 +11,48 @@

namespace ApiPlatform\Core\Action;

use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Util\ErrorFormatGuesser;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\SerializerInterface;

/**
* Renders a normalized exception for a given {@see \Symfony\Component\Debug\Exception\FlattenException}.
*
* Usage:
*
* $exceptionAction = new ExceptionAction(
* new Serializer(),
* [
* 'jsonproblem' => ['application/problem+json'],
* 'jsonld' => ['application/ld+json'],
* ],
* [
* ExceptionInterface::class => Response::HTTP_BAD_REQUEST,
* InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
* ]
* );
*
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com>
*/
final class ExceptionAction
{
const DEFAULT_EXCEPTION_TO_STATUS = [
ExceptionInterface::class => Response::HTTP_BAD_REQUEST,
InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
];

private $serializer;
private $errorFormats;
private $exceptionToStatus;

public function __construct(SerializerInterface $serializer, array $errorFormats, $exceptionToStatus = [])
/**
* @param SerializerInterface $serializer
* @param array $errorFormats A list of enabled formats, the first one will be the default
* @param array $exceptionToStatus A list of exceptions mapped to their HTTP status code
*/
public function __construct(SerializerInterface $serializer, array $errorFormats, array $exceptionToStatus = [])
{
$this->serializer = $serializer;
$this->errorFormats = $errorFormats;
$this->exceptionToStatus = self::DEFAULT_EXCEPTION_TO_STATUS + $exceptionToStatus;
$this->exceptionToStatus = $exceptionToStatus;
}

/**
Expand Down
Expand Up @@ -87,6 +87,7 @@ private function handleConfig(ContainerBuilder $container, array $config, array
$container->setParameter('api_platform.title', $config['title']);
$container->setParameter('api_platform.description', $config['description']);
$container->setParameter('api_platform.version', $config['version']);
$container->setParameter('api_platform.exception_to_status', $config['exception_to_status']);
$container->setParameter('api_platform.formats', $formats);
$container->setParameter('api_platform.error_formats', $errorFormats);
$container->setParameter('api_platform.collection.order', $config['collection']['order']);
Expand Down
61 changes: 60 additions & 1 deletion src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
Expand Up @@ -11,14 +11,19 @@

namespace ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection;

use ApiPlatform\Core\Exception\InvalidArgumentException;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Exception\ExceptionInterface;

/**
* The configuration of the bundle.
*
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
*/
final class Configuration implements ConfigurationInterface
{
Expand All @@ -41,7 +46,7 @@ public function getConfigTreeBuilder()
->booleanNode('enable_nelmio_api_doc')->defaultValue(false)->info('Enable the Nelmio Api doc integration.')->end()
->booleanNode('enable_swagger')->defaultValue(true)->info('Enable the Swagger documentation and export.')->end()

->arrayNode('collection')
->arrayNode('collection')
->addDefaultsIfNotSet()
->children()
->scalarNode('order')->defaultNull()->info('The default order of results.')->end()
Expand All @@ -64,6 +69,8 @@ public function getConfigTreeBuilder()
->end()
->end();

$this->addExceptionToStatusSection($rootNode);

$this->addFormatSection($rootNode, 'formats', [
'jsonld' => ['mime_types' => ['application/ld+json']],
'json' => ['mime_types' => ['application/json']], // Swagger support
Expand All @@ -77,6 +84,58 @@ public function getConfigTreeBuilder()
return $treeBuilder;
}

/**
* Adds an exception to status section.
*
* @param ArrayNodeDefinition $rootNode
*
* @throws InvalidConfigurationException
*/
private function addExceptionToStatusSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
->arrayNode('exception_to_status')
->defaultValue([
ExceptionInterface::class => Response::HTTP_BAD_REQUEST,
InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
])
->info('The list of exceptions mapped to their HTTP status code.')
->normalizeKeys(false)
->useAttributeAsKey('exception_class')
->beforeNormalization()
->ifArray()
->then(function (array $exceptionToStatus) {
foreach ($exceptionToStatus as &$httpStatusCode) {
if (is_int($httpStatusCode)) {
continue;
}

if (defined($httpStatusCodeConstant = sprintf('%s::%s', Response::class, $httpStatusCode))) {
$httpStatusCode = constant($httpStatusCodeConstant);
}
}

return $exceptionToStatus;
})
->end()
->prototype('integer')->end()
->validate()
->ifArray()
->then(function (array $exceptionToStatus) {
foreach ($exceptionToStatus as $httpStatusCode) {
if ($httpStatusCode < 100 || $httpStatusCode >= 600) {
throw new InvalidConfigurationException(sprintf('The HTTP status code "%s" is not valid.', $httpStatusCode));
}
}

return $exceptionToStatus;
})
->end()
->end()
->end();
}

/**
* Adds a format section.
*
Expand Down
1 change: 1 addition & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/api.xml
Expand Up @@ -168,6 +168,7 @@
<service id="api_platform.action.exception" class="ApiPlatform\Core\Action\ExceptionAction">
<argument type="service" id="api_platform.serializer" />
<argument>%api_platform.error_formats%</argument>
<argument>%api_platform.exception_to_status%</argument>
</service>

<!-- Cache -->
Expand Down
46 changes: 37 additions & 9 deletions tests/Action/ExceptionActionTest.php
Expand Up @@ -16,32 +16,60 @@
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\SerializerInterface;

/**
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
*/
class ExceptionActionTest extends \PHPUnit_Framework_TestCase
{
public function testGetException()
public function testActionWithCatchableException()
{
$flattenException = $this->prophesize(FlattenException::class);
$flattenException->getClass()->willReturn(InvalidArgumentException::class);
$flattenException->setStatusCode(Response::HTTP_BAD_REQUEST)->willReturn();
$flattenException->getHeaders()->willReturn(['Content-Type' => 'application/problem+json']);
$serializerException = $this->prophesize(ExceptionInterface::class);
$serializerException->willExtend(\Exception::class);

$flattenException = FlattenException::create($serializerException->reveal());

$flattenException->getStatusCode()->willReturn(Response::HTTP_BAD_REQUEST);
$serializer = $this->prophesize(SerializerInterface::class);
$exceptionAction = new ExceptionAction($serializer->reveal(), ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']]);
$serializer->serialize($flattenException, 'jsonproblem')->willReturn();

$exceptionAction = new ExceptionAction($serializer->reveal(), ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']], [ExceptionInterface::class => Response::HTTP_BAD_REQUEST, InvalidArgumentException::class => Response::HTTP_BAD_REQUEST]);

$request = new Request();
$request->setFormat('jsonproblem', 'application/problem+json');
$serializer->serialize($flattenException, 'jsonproblem')->willReturn();

$expected = new Response('', Response::HTTP_BAD_REQUEST, [
'Content-Type' => 'application/problem+json; charset=utf-8',
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'deny',
]);

$this->assertEquals($expected, $exceptionAction($flattenException->reveal(), $request));
$this->assertEquals($expected, $exceptionAction($flattenException, $request));
}

public function testActionWithUncatchableException()
{
$serializerException = $this->prophesize(ExceptionInterface::class);
$serializerException->willExtend(\Exception::class);

$flattenException = FlattenException::create($serializerException->reveal());

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

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

$request = new Request();
$request->setFormat('jsonproblem', 'application/problem+json');

$expected = new Response('', Response::HTTP_INTERNAL_SERVER_ERROR, [
'Content-Type' => 'application/problem+json; charset=utf-8',
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'deny',
]);

$this->assertEquals($expected, $exceptionAction($flattenException, $request));
}
}
Expand Up @@ -12,6 +12,7 @@
namespace ApiPlatform\Core\Tests\Bridge\Symfony\Bundle\DependencyInjection;

use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\ApiPlatformExtension;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use FOS\UserBundle\FOSUserBundle;
use Nelmio\ApiDocBundle\NelmioApiDocBundle;
Expand All @@ -23,6 +24,8 @@
use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Exception\ExceptionInterface;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
Expand Down Expand Up @@ -190,6 +193,7 @@ private function getContainerBuilderProphecy()
'api_platform.description' => 'description',
'api_platform.error_formats' => ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']],
'api_platform.formats' => ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']],
'api_platform.exception_to_status' => [ExceptionInterface::class => Response::HTTP_BAD_REQUEST, InvalidArgumentException::class => Response::HTTP_BAD_REQUEST],
'api_platform.title' => 'title',
'api_platform.version' => 'version',
];
Expand Down

0 comments on commit e9c1863

Please sign in to comment.