Skip to content

Commit 1b42894

Browse files
authored
fix: errors bc with rfc_7807_compliant_errors false (#5974)
* fix: errors bc with rfc_7807_compliant_errors false
1 parent 73569fc commit 1b42894

File tree

61 files changed

+1503
-746
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1503
-746
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ jobs:
204204
- name: Setup PHP
205205
uses: shivammathur/setup-php@v2
206206
with:
207-
php-version: latest
207+
php-version: ${{ matrix.php }}
208208
tools: pecl, composer
209209
extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite, mongodb
210210
ini-values: memory_limit=-1

docs/guides/error-provider.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
// ---
3+
// slug: error-provider
4+
// name: Error provider to translate exception messages
5+
// position: 7
6+
// executable: true
7+
// tags: design, state
8+
// ---
9+
10+
// Note that we use the following configuration:
11+
// ```
12+
// api_platform:
13+
// defaults:
14+
// rfc_7807_compliant_errors: true
15+
// ```
16+
// To customize the API Platform response, replace the api_platform.state.error_provider with your own provider:
17+
namespace App\ApiResource {
18+
use ApiPlatform\Metadata\ApiResource;
19+
use ApiPlatform\Metadata\Get;
20+
use ApiPlatform\Metadata\Operation;
21+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
22+
23+
#[ApiResource(
24+
operations: [
25+
new Get(provider: Book::class.'::provide'),
26+
],
27+
)]
28+
class Book
29+
{
30+
public function __construct(
31+
public readonly int $id = 1,
32+
public readonly string $name = 'Anon',
33+
) {
34+
}
35+
36+
public static function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
37+
{
38+
throw new BadRequestHttpException('something is not right');
39+
}
40+
}
41+
}
42+
43+
namespace App\State {
44+
use ApiPlatform\Metadata\Operation;
45+
use ApiPlatform\State\ApiResource\Error;
46+
use ApiPlatform\State\ProviderInterface;
47+
48+
// Note that we need to replace the "api_platform.state.error_provider" service, this is done later in this guide.
49+
final class ErrorProvider implements ProviderInterface
50+
{
51+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
52+
{
53+
$request = $context['request'];
54+
if (!$request || !($exception = $request->attributes->get('exception'))) {
55+
throw new \RuntimeException();
56+
}
57+
58+
/** @var \ApiPlatform\Metadata\HttpOperation $operation */
59+
$status = $operation->getStatus() ?? 500;
60+
// You don't have to use this, you can use a Response, an array or any object (preferably a resource that API Platform can handle).
61+
$error = Error::createFromException($exception, $status);
62+
63+
// care about hiding informations as this can be a security leak
64+
if ($status >= 500) {
65+
$error->setDetail('Something went wrong');
66+
} else {
67+
// You can handle translation here with the [Translator](https://symfony.com/doc/current/translation.html)
68+
$error->setDetail(str_replace('something is not right', 'les calculs ne sont pas bons', $exception->getMessage()));
69+
}
70+
71+
return $error;
72+
}
73+
}
74+
}
75+
76+
// This is replacing the service, the "key" is important as this is the provider we
77+
// will look for when handling an exception.
78+
namespace App\DependencyInjection {
79+
use App\State\ErrorProvider;
80+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
81+
82+
function configure(ContainerConfigurator $configurator): void
83+
{
84+
$services = $configurator->services();
85+
$services->set('api_platform.state.error_provider')
86+
->class(ErrorProvider::class)
87+
->tag('api_platform.state_provider', ['key' => 'api_platform.state.error_provider']);
88+
}
89+
90+
}
91+
92+
93+
namespace App\Tests {
94+
use ApiPlatform\Playground\Test\TestGuideTrait;
95+
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
96+
97+
final class BookTest extends ApiTestCase
98+
{
99+
use TestGuideTrait;
100+
101+
public function testBookDoesNotExists(): void
102+
{
103+
static::createClient()->request('GET', '/books/1', options: ['headers' => ['accept' => 'application/ld+json']]);
104+
$this->assertResponseStatusCodeSame(400);
105+
$this->assertJsonContains([
106+
'detail' => 'les calculs ne sont pas bons'
107+
]);
108+
}
109+
}
110+
}
111+
112+
namespace App\Playground {
113+
use Symfony\Component\HttpFoundation\Request;
114+
115+
function request(): Request
116+
{
117+
return Request::create('/books/1.jsonld', 'GET');
118+
}
119+
}

docs/guides/error-resource.php

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
// ---
3+
// slug: error-resource
4+
// name: Error resource for domain exceptions
5+
// position: 7
6+
// executable: true
7+
// tags: design, state
8+
// ---
9+
10+
// Note that we use the following configuration:
11+
// ```
12+
// api_platform:
13+
// defaults:
14+
// rfc_7807_compliant_errors: true
15+
// ```
16+
namespace App\ApiResource {
17+
use ApiPlatform\Metadata\ErrorResource;
18+
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
19+
20+
// We create a `MyDomainException` marked as an `ErrorResource`
21+
// It implements ProblemExceptionInterface as we want to be compatible with the [JSON Problem rfc7807](https://datatracker.ietf.org/doc/rfc7807/)
22+
#[ErrorResource]
23+
class MyDomainException extends \Exception implements ProblemExceptionInterface
24+
{
25+
public function getType(): string
26+
{
27+
return 'teapot';
28+
}
29+
30+
public function getTitle(): ?string
31+
{
32+
return null;
33+
}
34+
35+
public function getStatus(): ?int
36+
{
37+
return 418;
38+
}
39+
40+
public function getDetail(): ?string
41+
{
42+
return $this->getMessage();
43+
}
44+
45+
public function getInstance(): ?string
46+
{
47+
return null;
48+
}
49+
}
50+
51+
use ApiPlatform\Metadata\ApiResource;
52+
use ApiPlatform\Metadata\Get;
53+
use ApiPlatform\Metadata\Operation;
54+
55+
#[ApiResource(
56+
operations: [
57+
new Get(provider: Book::class.'::provide'),
58+
],
59+
)]
60+
class Book
61+
{
62+
public function __construct(
63+
public readonly int $id = 1,
64+
public readonly string $name = 'Anon',
65+
) {
66+
}
67+
68+
public static function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
69+
{
70+
// We throw our domain exception
71+
throw new MyDomainException('I am teapot');
72+
}
73+
}
74+
75+
}
76+
77+
namespace App\Tests {
78+
use ApiPlatform\Playground\Test\TestGuideTrait;
79+
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
80+
81+
final class BookTest extends ApiTestCase
82+
{
83+
use TestGuideTrait;
84+
85+
public function testBookDoesNotExists(): void
86+
{
87+
static::createClient()->request('GET', '/books/1', options: ['headers' => ['accept' => 'application/ld+json']]);
88+
// We expect the status code returned by our `getStatus` and the message inside `detail`
89+
// for security reasons 500 errors will get their "detail" changed by our Error Provider
90+
// you can override this by looking at the [Error Provider guide](/docs/guides/error-provider).
91+
$this->assertResponseStatusCodeSame(418);
92+
$this->assertJsonContains([
93+
'detail' => 'I am teapot'
94+
]);
95+
}
96+
}
97+
}
98+
99+
namespace App\Playground {
100+
use Symfony\Component\HttpFoundation\Request;
101+
102+
function request(): Request
103+
{
104+
return Request::create('/books/1.jsonld', 'GET');
105+
}
106+
}
107+

features/hal/problem.feature

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@!mongodb
12
Feature: Error handling valid according to RFC 7807 (application/problem+json)
23
In order to be able to handle error client side
34
As a client software developer
@@ -6,7 +7,7 @@ Feature: Error handling valid according to RFC 7807 (application/problem+json)
67
Scenario: Get an error
78
When I add "Content-Type" header equal to "application/json"
89
And I add "Accept" header equal to "application/json"
9-
And I send a "POST" request to "/dummies" with body:
10+
And I send a "POST" request to "/dummy_problems" with body:
1011
"""
1112
{}
1213
"""
@@ -33,7 +34,7 @@ Feature: Error handling valid according to RFC 7807 (application/problem+json)
3334
Scenario: Get an error during deserialization of simple relation
3435
When I add "Content-Type" header equal to "application/json"
3536
And I add "Accept" header equal to "application/json"
36-
And I send a "POST" request to "/dummies" with body:
37+
And I send a "POST" request to "/dummy_problems" with body:
3738
"""
3839
{
3940
"name": "Foo",

features/hal/problem_legacy.feature

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 "Content-Type" header equal to "application/json"
8+
And I add "Accept" header equal to "application/json"
9+
And I send a "POST" request to "/dummies" with body:
10+
"""
11+
{}
12+
"""
13+
Then the response status code should be 422
14+
And the response should be in JSON
15+
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
16+
And the JSON should be equal to:
17+
"""
18+
{
19+
"type": "https:\/\/tools.ietf.org\/html\/rfc2616#section-10",
20+
"title": "An error occurred",
21+
"detail": "name: This value should not be blank.",
22+
"violations": [
23+
{
24+
"propertyPath": "name",
25+
"message": "This value should not be blank.",
26+
"code": "c1051bb4-d103-4f74-8988-acbcafc7fdc3"
27+
}
28+
]
29+
}
30+
"""
31+
32+
Scenario: Get an error during deserialization of simple relation
33+
When I add "Content-Type" header equal to "application/json"
34+
And I add "Accept" header equal to "application/json"
35+
And I send a "POST" request to "/dummies" with body:
36+
"""
37+
{
38+
"name": "Foo",
39+
"relatedDummy": {
40+
"name": "bar"
41+
}
42+
}
43+
"""
44+
Then the response status code should be 400
45+
And the response should be in JSON
46+
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
47+
And the JSON node "type" should be equal to "https://tools.ietf.org/html/rfc2616#section-10"
48+
And the JSON node "title" should be equal to "An error occurred"
49+
And the JSON node "detail" should be equal to 'Nested documents for attribute "relatedDummy" are not allowed. Use IRIs instead.'
50+
And the JSON node "trace" should exist

0 commit comments

Comments
 (0)