Skip to content

Commit 95fb25c

Browse files
authored
Merge pull request #221 from akeneo/API-1690-error-handling
Api 1690 error handling
2 parents ebb0c1c + 7bbf258 commit 95fb25c

File tree

7 files changed

+280
-2
lines changed

7 files changed

+280
-2
lines changed

spec/Client/HttpExceptionHandlerSpec.php

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44

55
use Akeneo\Pim\ApiClient\Exception\BadRequestHttpException;
66
use Akeneo\Pim\ApiClient\Exception\ClientErrorHttpException;
7+
use Akeneo\Pim\ApiClient\Exception\ForbiddenHttpException;
8+
use Akeneo\Pim\ApiClient\Exception\MethodNotAllowedHttpException;
9+
use Akeneo\Pim\ApiClient\Exception\NotAcceptableHttpException;
710
use Akeneo\Pim\ApiClient\Exception\NotFoundHttpException;
811
use Akeneo\Pim\ApiClient\Exception\RedirectionHttpException;
912
use Akeneo\Pim\ApiClient\Exception\ServerErrorHttpException;
13+
use Akeneo\Pim\ApiClient\Exception\TooManyRequestsHttpException;
1014
use Akeneo\Pim\ApiClient\Exception\UnauthorizedHttpException;
1115
use Akeneo\Pim\ApiClient\Exception\UnprocessableEntityHttpException;
1216
use Akeneo\Pim\ApiClient\Client\HttpExceptionHandler;
17+
use Akeneo\Pim\ApiClient\Exception\UnsupportedMediaTypeHttpException;
1318
use PhpSpec\ObjectBehavior;
1419
use Psr\Http\Message\RequestInterface;
1520
use Psr\Http\Message\ResponseInterface;
@@ -82,6 +87,26 @@ function it_throws_unauthorized_request_exception_when_status_code_401(
8287
->during('transformResponseToException', [$request, $response]);
8388
}
8489

90+
function it_throws_forbidden_exception_when_status_code_403(
91+
RequestInterface $request,
92+
ResponseInterface $response,
93+
StreamInterface $responseBody
94+
) {
95+
$response->getStatusCode()->willReturn(403);
96+
$response->getBody()->willReturn($responseBody);
97+
$responseBody->getContents()->willReturn('{"code": 403, "message": "Access forbidden."}');
98+
$responseBody->rewind()->shouldBeCalled();
99+
$this
100+
->shouldThrow(
101+
new ForbiddenHttpException(
102+
'Access forbidden.',
103+
$request->getWrappedObject(),
104+
$response->getWrappedObject()
105+
)
106+
)
107+
->during('transformResponseToException', [$request, $response]);
108+
}
109+
85110
function it_throws_not_found_exception_when_status_code_404(
86111
RequestInterface $request,
87112
ResponseInterface $response,
@@ -102,6 +127,81 @@ function it_throws_not_found_exception_when_status_code_404(
102127
->during('transformResponseToException', [$request, $response]);
103128
}
104129

130+
function it_throws_method_not_allowed_exception_when_status_code_405(
131+
RequestInterface $request,
132+
ResponseInterface $response,
133+
StreamInterface $responseBody
134+
) {
135+
$response->getStatusCode()->willReturn(405);
136+
$response->getBody()->willReturn($responseBody);
137+
$responseBody->getContents()->willReturn(<<<JSON
138+
{
139+
"code": 405,
140+
"message": "No route found for 'POST /api/rest/v1/products/myproduct': Method Not Allowed (Allow: GET, PATCH, DELETE)"
141+
}
142+
JSON);
143+
$responseBody->rewind()->shouldBeCalled();
144+
$this
145+
->shouldThrow(
146+
new MethodNotAllowedHttpException(
147+
'No route found for \'POST /api/rest/v1/products/myproduct\': Method Not Allowed (Allow: GET, PATCH, DELETE)',
148+
$request->getWrappedObject(),
149+
$response->getWrappedObject()
150+
)
151+
)
152+
->during('transformResponseToException', [$request, $response]);
153+
}
154+
155+
function it_throws_method_not_allowed_exception_when_status_code_406(
156+
RequestInterface $request,
157+
ResponseInterface $response,
158+
StreamInterface $responseBody
159+
) {
160+
$response->getStatusCode()->willReturn(406);
161+
$response->getBody()->willReturn($responseBody);
162+
$responseBody->getContents()->willReturn(<<<JSON
163+
{
164+
"code": 406,
165+
"message": "‘xxx’ in ‘Accept‘ header is not valid. Only ‘application/json‘ is allowed."
166+
}
167+
JSON);
168+
$responseBody->rewind()->shouldBeCalled();
169+
$this
170+
->shouldThrow(
171+
new NotAcceptableHttpException(
172+
'‘xxx’ in ‘Accept‘ header is not valid. Only ‘application/json‘ is allowed.',
173+
$request->getWrappedObject(),
174+
$response->getWrappedObject()
175+
)
176+
)
177+
->during('transformResponseToException', [$request, $response]);
178+
}
179+
180+
function it_throws_method_not_allowed_exception_when_status_code_415(
181+
RequestInterface $request,
182+
ResponseInterface $response,
183+
StreamInterface $responseBody
184+
) {
185+
$response->getStatusCode()->willReturn(415);
186+
$response->getBody()->willReturn($responseBody);
187+
$responseBody->getContents()->willReturn(<<<JSON
188+
{
189+
"code": 415,
190+
"message": "The ‘Content-type’ header is missing. ‘application/json’ has to specified as value."
191+
}
192+
JSON);
193+
$responseBody->rewind()->shouldBeCalled();
194+
$this
195+
->shouldThrow(
196+
new UnsupportedMediaTypeHttpException(
197+
'The ‘Content-type’ header is missing. ‘application/json’ has to specified as value.',
198+
$request->getWrappedObject(),
199+
$response->getWrappedObject()
200+
)
201+
)
202+
->during('transformResponseToException', [$request, $response]);
203+
}
204+
105205
function it_throws_bad_request_exception_when_status_code_422(
106206
RequestInterface $request,
107207
ResponseInterface $response,
@@ -122,14 +222,34 @@ function it_throws_bad_request_exception_when_status_code_422(
122222
->during('transformResponseToException', [$request, $response]);
123223
}
124224

225+
function it_throws_bad_request_exception_when_status_code_429(
226+
RequestInterface $request,
227+
ResponseInterface $response,
228+
StreamInterface $responseBody
229+
) {
230+
$response->getStatusCode()->willReturn(429);
231+
$response->getBody()->willReturn($responseBody);
232+
$responseBody->getContents()->willReturn('Too Many Requests');
233+
$responseBody->getContents()->shouldBeCalled();
234+
$this
235+
->shouldThrow(
236+
new TooManyRequestsHttpException(
237+
'Too Many Requests',
238+
$request->getWrappedObject(),
239+
$response->getWrappedObject()
240+
)
241+
)
242+
->during('transformResponseToException', [$request, $response]);
243+
}
244+
125245
function it_throws_bad_request_exception_when_status_code_4xx(
126246
RequestInterface $request,
127247
ResponseInterface $response,
128248
StreamInterface $responseBody
129249
) {
130-
$response->getStatusCode()->willReturn(405);
250+
$response->getStatusCode()->willReturn(418);
131251
$response->getBody()->willReturn($responseBody);
132-
$responseBody->getContents()->willReturn('{"code": 405, "message": "Not allowed."}');
252+
$responseBody->getContents()->willReturn('{"code": 418, "message": "Not allowed."}');
133253
$responseBody->rewind()->shouldBeCalled();
134254
$this
135255
->shouldThrow(

src/Client/HttpExceptionHandler.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55
use Akeneo\Pim\ApiClient\Exception\BadRequestHttpException;
66
use Akeneo\Pim\ApiClient\Exception\ClientErrorHttpException;
77
use Akeneo\Pim\ApiClient\Exception\ForbiddenHttpException;
8+
use Akeneo\Pim\ApiClient\Exception\MethodNotAllowedHttpException;
9+
use Akeneo\Pim\ApiClient\Exception\NotAcceptableHttpException;
810
use Akeneo\Pim\ApiClient\Exception\NotFoundHttpException;
911
use Akeneo\Pim\ApiClient\Exception\RedirectionHttpException;
1012
use Akeneo\Pim\ApiClient\Exception\ServerErrorHttpException;
13+
use Akeneo\Pim\ApiClient\Exception\TooManyRequestsHttpException;
1114
use Akeneo\Pim\ApiClient\Exception\UnauthorizedHttpException;
1215
use Akeneo\Pim\ApiClient\Exception\UnprocessableEntityHttpException;
16+
use Akeneo\Pim\ApiClient\Exception\UnsupportedMediaTypeHttpException;
1317
use Psr\Http\Message\RequestInterface;
1418
use Psr\Http\Message\ResponseInterface;
1519

@@ -61,10 +65,26 @@ public function transformResponseToException(
6165
throw new NotFoundHttpException($this->getResponseMessage($response), $request, $response);
6266
}
6367

68+
if (HttpClient::HTTP_METHOD_NOT_ALLOWED === $response->getStatusCode()) {
69+
throw new MethodNotAllowedHttpException($this->getResponseMessage($response), $request, $response);
70+
}
71+
72+
if (HttpClient::HTTP_NOT_ACCEPTABLE === $response->getStatusCode()) {
73+
throw new NotAcceptableHttpException($this->getResponseMessage($response), $request, $response);
74+
}
75+
76+
if (HttpClient::HTTP_UNSUPPORTED_MEDIA_TYPE === $response->getStatusCode()) {
77+
throw new UnsupportedMediaTypeHttpException($this->getResponseMessage($response), $request, $response);
78+
}
79+
6480
if (HttpClient::HTTP_UNPROCESSABLE_ENTITY === $response->getStatusCode()) {
6581
throw new UnprocessableEntityHttpException($this->getResponseMessage($response), $request, $response);
6682
}
6783

84+
if (HttpClient::HTTP_TOO_MANY_REQUESTS === $response->getStatusCode()) {
85+
throw new TooManyRequestsHttpException($response->getBody()->getContents(), $request, $response);
86+
}
87+
6888
if ($this->isApiClientErrorStatusCode($response->getStatusCode())) {
6989
throw new ClientErrorHttpException($this->getResponseMessage($response), $request, $response);
7090
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Akeneo\Pim\ApiClient\Exception;
4+
5+
/**
6+
* @copyright 2022 Akeneo SAS (https://www.akeneo.com)
7+
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
8+
*/
9+
class MethodNotAllowedHttpException extends ClientErrorHttpException
10+
{
11+
protected function getAdditionalInformationMessage(): string
12+
{
13+
return '(see https://api.akeneo.com/php-client/exception.html#method-not-allowed-exception)';
14+
}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Akeneo\Pim\ApiClient\Exception;
4+
5+
/**
6+
* @copyright 2022 Akeneo SAS (https://www.akeneo.com)
7+
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
8+
*/
9+
class NotAcceptableHttpException extends ClientErrorHttpException
10+
{
11+
protected function getAdditionalInformationMessage(): string
12+
{
13+
return '(see https://api.akeneo.com/php-client/exception.html#not-acceptable-exception)';
14+
}
15+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace Akeneo\Pim\ApiClient\Exception;
4+
5+
/**
6+
* @copyright 2022 Akeneo SAS (https://www.akeneo.com)
7+
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
8+
*/
9+
class TooManyRequestsHttpException extends ClientErrorHttpException
10+
{
11+
/**
12+
* How much time must be waited before next request. Result in seconds.
13+
*/
14+
public function getRetryAfter(): int
15+
{
16+
$response = $this->getResponse();
17+
18+
if (!$response->hasHeader('Retry-After')) {
19+
throw new RuntimeException('Cannot find Retry-After header.');
20+
}
21+
22+
$retryAfter = $response->getHeader('Retry-After')[0];
23+
24+
if (preg_match('/^\d+$/', $retryAfter)) {
25+
return (int) $retryAfter;
26+
}
27+
28+
throw new RuntimeException('Cannot parse Retry-After header. Value must be seconds.');
29+
}
30+
31+
protected function getAdditionalInformationMessage(): string
32+
{
33+
return '(see https://api.akeneo.com/php-client/exception.html#too-many-requests-exception)';
34+
}
35+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Akeneo\Pim\ApiClient\Exception;
4+
5+
/**
6+
* @copyright 2022 Akeneo SAS (https://www.akeneo.com)
7+
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
8+
*/
9+
class UnsupportedMediaTypeHttpException extends ClientErrorHttpException
10+
{
11+
protected function getAdditionalInformationMessage(): string
12+
{
13+
return '(see https://api.akeneo.com/php-client/exception.html#unsupported-media-type-exception)';
14+
}
15+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace Akeneo\Pim\ApiClient\tests\Exception;
4+
5+
use Akeneo\Pim\ApiClient\Exception\RuntimeException;
6+
use Akeneo\Pim\ApiClient\Exception\TooManyRequestsHttpException;
7+
use PHPUnit\Framework\TestCase;
8+
use Psr\Http\Message\RequestInterface;
9+
use Psr\Http\Message\ResponseInterface;
10+
11+
class TooManyRequestsHttpExceptionTest extends TestCase
12+
{
13+
private RequestInterface $request;
14+
15+
public function setUp(): void
16+
{
17+
$this->request = $this->createMock(RequestInterface::class);
18+
}
19+
20+
public function testRetryAfter(): void
21+
{
22+
$response = $this->createMock(ResponseInterface::class);
23+
$response->method('hasHeader')->willReturn(true);
24+
$response->method('getHeader')->willReturn(['10']);
25+
26+
$exception = new TooManyRequestsHttpException('Too Many Requests', $this->request, $response, null);
27+
28+
$this->assertSame(
29+
10,
30+
$exception->getRetryAfter()
31+
);
32+
}
33+
34+
public function testCannotFindRetryAfterHeader(): void
35+
{
36+
$response = $this->createMock(ResponseInterface::class);
37+
$response->method('hasHeader')->willReturn(false);
38+
39+
$exception = new TooManyRequestsHttpException('Too Many Requests', $this->request, $response, null);
40+
41+
$this->expectException(RuntimeException::class);
42+
43+
$exception->getRetryAfter();
44+
}
45+
46+
public function testCannotParseRetryAfterHeader(): void
47+
{
48+
$response = $this->createMock(ResponseInterface::class);
49+
$response->method('hasHeader')->willReturn(true);
50+
$response->method('getHeader')->willReturn([0 => 'Wed, 21 Oct 2015 07:28:00 GMT']);
51+
52+
$exception = new TooManyRequestsHttpException('Too Many Requests', $this->request, $response, null);
53+
54+
$this->expectException(RuntimeException::class);
55+
56+
$exception->getRetryAfter();
57+
}
58+
}

0 commit comments

Comments
 (0)