Skip to content

Commit 095c4ee

Browse files
committed
Add suport for the API Problem format
1 parent 86a57d6 commit 095c4ee

File tree

15 files changed

+514
-7
lines changed

15 files changed

+514
-7
lines changed

features/problem.feature

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Feature: Error handling valid according to RFC 7807 (application/problem+json)
2+
In order to be able to handle error client side
3+
As a client software developer
4+
I need to retrieve an RFC 7807 compliant serialization of errors
5+
6+
Scenario: Get an error
7+
When I add "Accept" header equal to "application/json"
8+
And I send a "POST" request to "/dummies" with body:
9+
"""
10+
{}
11+
"""
12+
Then the response status code should be 400
13+
And the response should be in JSON
14+
And the header "Content-Type" should be equal to "application/problem+json"
15+
And the JSON should be equal to:
16+
"""
17+
{
18+
"type": "https://tools.ietf.org/html/rfc2616#section-10",
19+
"title": "An error occurred",
20+
"detail": "name: This value should not be blank.",
21+
"violations": [
22+
{
23+
"propertyPath": "name",
24+
"message": "This value should not be blank."
25+
}
26+
]
27+
}
28+
"""
29+
30+
Scenario: Get an error during deserialization of simple relation
31+
When I add "Accept" header equal to "application/json"
32+
And I send a "POST" request to "/dummies" with body:
33+
"""
34+
{
35+
"name": "Foo",
36+
"relatedDummy": {
37+
"name": "bar"
38+
}
39+
}
40+
"""
41+
Then the response status code should be 400
42+
And the response should be in JSON
43+
And the header "Content-Type" should be equal to "application/problem+json"
44+
And the JSON node "type" should be equal to "https://tools.ietf.org/html/rfc2616#section-10"
45+
And the JSON node "title" should be equal to "An error occurred"
46+
And the JSON node "detail" should be equal to 'Nested objects for attribute "relatedDummy" of "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy" are not enabled. Use serialization groups to change that behavior.'
47+
And the JSON node "trace" should exist

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,13 @@ public function load(array $configs, ContainerBuilder $container)
6060
}
6161

6262
$formats = $this->getFormats($config['formats']);
63+
$errorFormats = $this->getFormats($config['error_formats']);
6364

6465
$container->setParameter('api_platform.title', $config['title']);
6566
$container->setParameter('api_platform.description', $config['description']);
6667
$container->setParameter('api_platform.version', $config['version']);
6768
$container->setParameter('api_platform.formats', $formats);
68-
$container->setParameter('api_platform.error_formats', $this->getFormats($config['error_formats']));
69+
$container->setParameter('api_platform.error_formats', $errorFormats);
6970
$container->setParameter('api_platform.collection.order', $config['collection']['order']);
7071
$container->setParameter('api_platform.collection.order_parameter_name', $config['collection']['order_parameter_name']);
7172
$container->setParameter('api_platform.collection.pagination.enabled', $config['collection']['pagination']['enabled']);
@@ -95,6 +96,10 @@ public function load(array $configs, ContainerBuilder $container)
9596
$loader->load('hal.xml');
9697
}
9798

99+
if (isset($errorFormats['jsonproblem'])) {
100+
$loader->load('problem.xml');
101+
}
102+
98103
$this->registerAnnotationLoaders($container);
99104
$this->registerFileLoaders($container);
100105

src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ public function getConfigTreeBuilder()
6969
->end();
7070

7171
$this->addFormatSection($rootNode, 'formats', ['jsonld' => ['mime_types' => ['application/ld+json']]]);
72-
$this->addFormatSection($rootNode, 'error_formats', ['jsonld' => ['mime_types' => ['application/ld+json']]]);
72+
$this->addFormatSection($rootNode, 'error_formats', [
73+
'jsonproblem' => ['mime_types' => ['application/problem+json']],
74+
'jsonld' => ['mime_types' => ['application/ld+json']],
75+
]);
7376

7477
return $treeBuilder;
7578
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
<!-- Serializer -->
9+
<service id="api_platform.problem.encoder" class="ApiPlatform\Core\Serializer\JsonEncoder" public="false">
10+
<argument>jsonproblem</argument>
11+
12+
<tag name="serializer.encoder" />
13+
</service>
14+
15+
<service id="api_platform.problem.normalizer.constraint_violation_list" class="ApiPlatform\Core\Problem\Serializer\ConstraintViolationListNormalizer" public="false">
16+
<tag name="serializer.normalizer" priority="16" />
17+
</service>
18+
19+
<service id="api_platform.problem.normalizer.error" class="ApiPlatform\Core\Problem\Serializer\ErrorNormalizer" public="false">
20+
<argument>%kernel.debug%</argument>
21+
22+
<tag name="serializer.normalizer" priority="8" />
23+
</service>
24+
</services>
25+
26+
</container>

src/Hydra/Serializer/ErrorNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ final class ErrorNormalizer implements NormalizerInterface
2828
private $urlGenerator;
2929
private $debug;
3030

31-
public function __construct(UrlGeneratorInterface $urlGenerator, bool $debug)
31+
public function __construct(UrlGeneratorInterface $urlGenerator, bool $debug = false)
3232
{
3333
$this->urlGenerator = $urlGenerator;
3434
$this->debug = $debug;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace ApiPlatform\Core\Problem\Serializer;
13+
14+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
15+
use Symfony\Component\Validator\ConstraintViolationListInterface;
16+
17+
/**
18+
* Converts {@see \Symfony\Component\Validator\ConstraintViolationListInterface} the API Problem spec (RFC 7807).
19+
*
20+
* @see https://tools.ietf.org/html/rfc7807
21+
22+
* @author Kévin Dunglas <[email protected]>
23+
*/
24+
final class ConstraintViolationListNormalizer implements NormalizerInterface
25+
{
26+
const FORMAT = 'jsonproblem';
27+
28+
/**
29+
* {@inheritdoc}
30+
*/
31+
public function normalize($object, $format = null, array $context = [])
32+
{
33+
$violations = [];
34+
$messages = [];
35+
36+
foreach ($object as $violation) {
37+
$violations[] = [
38+
'propertyPath' => $violation->getPropertyPath(),
39+
'message' => $violation->getMessage(),
40+
];
41+
42+
$propertyPath = $violation->getPropertyPath();
43+
$prefix = $propertyPath ? sprintf('%s: ', $propertyPath) : '';
44+
45+
$messages [] = $prefix.$violation->getMessage();
46+
}
47+
48+
return [
49+
'type' => $context['type'] ?? 'https://tools.ietf.org/html/rfc2616#section-10',
50+
'title' => $context['title'] ?? 'An error occurred',
51+
'detail' => $messages ? implode("\n", $messages) : (string) $object,
52+
'violations' => $violations,
53+
];
54+
}
55+
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public function supportsNormalization($data, $format = null)
60+
{
61+
return self::FORMAT === $format && $data instanceof ConstraintViolationListInterface;
62+
}
63+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace ApiPlatform\Core\Problem\Serializer;
13+
14+
use Symfony\Component\Debug\Exception\FlattenException;
15+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
16+
17+
/**
18+
* Normalizes errors according to the API Problem spec (RFC 7807).
19+
*
20+
* @see https://tools.ietf.org/html/rfc7807
21+
*
22+
* @author Kévin Dunglas <[email protected]>
23+
*/
24+
final class ErrorNormalizer implements NormalizerInterface
25+
{
26+
const FORMAT = 'jsonproblem';
27+
28+
private $debug;
29+
30+
public function __construct(bool $debug = false)
31+
{
32+
$this->debug = $debug;
33+
}
34+
35+
/**
36+
* {@inheritdoc}
37+
*/
38+
public function normalize($object, $format = null, array $context = [])
39+
{
40+
$message = $object->getMessage();
41+
if ($this->debug) {
42+
$trace = $object->getTrace();
43+
}
44+
45+
$data = [
46+
'type' => $context['type'] ?? 'https://tools.ietf.org/html/rfc2616#section-10',
47+
'title' => $context['title'] ?? 'An error occurred',
48+
'detail' => $message ?? (string) $object,
49+
];
50+
51+
if (isset($trace)) {
52+
$data['trace'] = $trace;
53+
}
54+
55+
return $data;
56+
}
57+
58+
/**
59+
* {@inheritdoc}
60+
*/
61+
public function supportsNormalization($data, $format = null)
62+
{
63+
return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException);
64+
}
65+
}

src/Util/ErrorFormatGuesser.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ abstract class ErrorFormatGuesser
3131
public static function guessErrorFormat(Request $request, array $errorFormats) : array
3232
{
3333
$requestFormat = $request->getRequestFormat(null);
34-
if (null === $requestFormat || !isset($errorFormats[$requestFormat])) {
34+
if (null !== $requestFormat && isset($errorFormats[$requestFormat])) {
3535
return ['key' => $requestFormat, 'value' => $errorFormats[$requestFormat]];
3636
}
3737

tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ private function getContainerBuilderProphecy()
177177
'api_platform.description' => 'description',
178178
'api_platform.version' => 'version',
179179
'api_platform.formats' => ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']],
180-
'api_platform.error_formats' => ['jsonld' => ['application/ld+json']],
180+
'api_platform.error_formats' => ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']],
181181
'api_platform.collection.order' => null,
182182
'api_platform.collection.order_parameter_name' => 'order',
183183
'api_platform.collection.pagination.enabled' => true,
@@ -305,7 +305,9 @@ private function getContainerBuilderProphecy()
305305
'api_platform.hydra.normalizer.collection_filters',
306306
'api_platform.hydra.normalizer.constraint_violation_list',
307307
'api_platform.hydra.normalizer.error',
308-
308+
'api_platform.problem.encoder',
309+
'api_platform.problem.normalizer.constraint_violation_list',
310+
'api_platform.problem.normalizer.error',
309311
];
310312

311313
foreach ($definitions as $definition) {

tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ public function testDefaultConfig()
3535
'description' => 'description',
3636
'version' => '1.0.0',
3737
'formats' => ['jsonld' => ['mime_types' => ['application/ld+json']]],
38-
'error_formats' => ['jsonld' => ['mime_types' => ['application/ld+json']]],
38+
'error_formats' => [
39+
'jsonproblem' => ['mime_types' => ['application/problem+json']],
40+
'jsonld' => ['mime_types' => ['application/ld+json']],
41+
],
3942
'naming' => [
4043
'resource_path_naming_strategy' => 'api_platform.naming.resource_path_naming_strategy.underscore',
4144
],

0 commit comments

Comments
 (0)