8000 [Feature] Render JSON API errors if the client has request them · tekord/laravel-json-api@9ee72ab · GitHub
[go: up one dir, main page]

Skip to content
This repository was archived by the owner on Feb 17, 2023. It is now read-only.

Commit 9ee72ab

Browse files
committed
[Feature] Render JSON API errors if the client has request them
Previously errors were only rendered as JSON API errors if a request was being routed to a JSON API endpoint. This meant that any errors occurring before routing to a JSON API endpoint were not rendered as JSON API even if the client had specified JSON API in their `Accept` header. This commit adds checking of what the client has requested to ensure that exceptions are always rendered as JSON API responses if the client has specified `application/vnd.api+json` in their Accept header.
1 parent 7413b82 commit 9ee72ab

File tree

9 files changed

+170
-14
lines changed

9 files changed

+170
-14
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. This projec
44

55
## Unreleased
66

7+
### Added
8+
- Errors that occur *before* a route is processed by a JSON API are now sent to the client as JSON API
9+
error responses if the client wants a JSON API response. This is determined using the `Accept` header
10+
and means that exceptions such as the maintenance mode exception are correctly returned as JSON API errors
11+
if that is what the client wants.
12+
713
### Changed
814
- Field guarding that was previously available on the Eloquent adapter is now also available on the
915
generic adapter.

docs/installation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class Handler extends ExceptionHandler
7070

7171
public function render($request, Exception $e)
7272
{
73-
if ($this->isJsonApi()) {
73+
if ($this->isJsonApi($request, $e)) {
7474
return $this->renderJsonApi($request, $e);
7575
}
7676

docs/upgrade.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,40 @@ and then we are planning on tagging `1.0.0` after a limited number of beta tags.
88

99
## 1.0.0-alpha.2 to 1.0.0-alpha.3
1010

11+
### Exception Handler
12+
13+
The `isJsonApi()` method on the `HandlesError` trait now requires the request and exception as arguments.
14+
This is so that a JSON API error response can be rendered if a client has requested JSON API via the request
15+
`Accept` header, but the request is not being processed by one of the configured APIs. This enables exceptions
16+
that are thrown *prior* to routing to be rendered as JSON API - for example, when the application is in
17+
maintenance mode.
18+
19+
You need to change this:
20+
21+
```php
22+
public function render($request, Exception $e)
23+
{
24+
if ($this->isJsonApi()) {
25+
return $this->renderJsonApi($request, $e);
26+
}
27+
28+
// ...
29+
}
30+
```
31+
32+
To this:
33+
34+
```php
35+
public function render($request, Exception $e)
36+
{
37+
if ($this->isJsonApi($request, $e)) {
38+
return $this->renderJsonApi($request, $e);
39+
}
40+
41+
// ...
42+
}
43+
```
44+
1145
### Not By Resource Resolution
1246

1347
When using *not-by-resource* resolution, the type of the class is now appended to the class name. E.g.

src/Encoder/Encoder.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use CloudCreativity\LaravelJsonApi\Contracts\Encoder\SerializerInterface;
2222
use CloudCreativity\LaravelJsonApi\Factories\Factory;
2323
use Neomerx\JsonApi\Encoder\Encoder as BaseEncoder;
24+
use Neomerx\JsonApi\Encoder\EncoderOptions;
2425
use Neomerx\JsonApi\Encoder\Serialize\ArraySerializerTrait;
2526

2627
/**

src/Exceptions/HandlesErrors.php

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@
1818

1919
namespace CloudCreativity\LaravelJsonApi\Exceptions;
2020

21+
use CloudCreativity\LaravelJsonApi\Contracts\Encoder\SerializerInterface;
2122
use CloudCreativity\LaravelJsonApi\Contracts\Exceptions\ExceptionParserInterface;
23+
use CloudCreativity\LaravelJsonApi\Encoder\Encoder;
2224
use CloudCreativity\LaravelJsonApi\Services\JsonApiService;
25+
use CloudCreativity\LaravelJsonApi\Utils\Helpers;
2326
use Exception;
2427
use Illuminate\Http\Request;
2528
use Illuminate\Http\Response;
29+
use Neomerx\JsonApi\Http\Headers\MediaType;
2630

2731
/**
2832
* Class HandlerTrait
@@ -33,10 +37,24 @@ trait HandlesErrors
3337
{
3438

3539
/**
40+
* Does the HTTP request require a JSON API error response?
41+
*
42+
* This method determines if we need to render a JSON API error response
43+
* for the provided exception. We need to do this if:
44+
*
45+
* - The client has requested JSON API via its Accept header; or
46+
* - The application is handling a request to a JSON API endpoint.
47+
*
48+
* @param Request $request
49+
* @param Exception $e
3650
* @return bool
3751
*/
38-
public function isJsonApi()
52+
public function isJsonApi($request, Exception $e)
3953
{
54+
if (Helpers::wantsJsonApi($request)) {
55+
return true;
56+
}
57+
4058
/** @var JsonApiService $service */
4159
$service = app(JsonApiService::class);
4260

@@ -48,7 +66,7 @@ public function isJsonApi()
4866
* @param Exception $e
4967
* @return Response
5068
*/
51-
public function renderJsonApi(Request $request, Exception $e)
69+
public function renderJsonApi($request, Exception $e)
5270
{
5371
< 10000 span class=pl-c>/** @var JsonApiService $service */
5472
$service = app(JsonApiService::class);
@@ -63,10 +81,19 @@ public function renderJsonApi(Request $request, Exception $e)
6381
return response('', Response::HTTP_NOT_ACCEPTABLE);
6482
}
6583

66-
return $service
67-
->requestApi()
68-
->response()
69-
->errors($response);
84+
/** If there is an active API, use that to send the response. */
85+
if ($api = $service->requestApi()) {
86+
return $api->response()->errors($response);
87+
}
88+
89+
/** @var SerializerInterface $serializer */
90+
$serializer = Encoder::instance();
91+
92+
return response()->json(
93+
$serializer->serializeErrors($response->getErrors()),
94+
$response->getHttpCode(),
95+
['Content-Type' => MediaType::JSON_API_MEDIA_TYPE]
96+
);
7097
}
7198

7299
}

src/Testing/TestExceptionHandler.php

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,8 @@
1919

2020
use CloudCreativity\LaravelJsonApi\Exceptions\HandlesErrors;
2121
use Exception;
22-
use Illuminate\Auth\Access\AuthorizationException;
23-
use Illuminate\Auth\AuthenticationException;
24-
use Neomerx\JsonApi\Exceptions\JsonApiException;
2522
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
23+
use Neomerx\JsonApi\Exceptions\JsonApiException;
2624

2725
/**
2826
* Class TestExceptionHandler
@@ -53,11 +51,16 @@ class TestExceptionHandler extends ExceptionHandler
5351

5452
/**
5553
* @var array
54+
* @todo when dropping support for Laravel 5.4, will no longer need to list these framework classes.
5655
*/
5756
protected $dontReport = [
57+
\Illuminate\Auth\AuthenticationException::class,
58+
\Illuminate\Auth\Access\AuthorizationException::class,
59+
\Symfony\Component\HttpKernel\Exception\HttpException::class,
60+
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
61+
\Illuminate\Session\TokenMismatchException::class,
62+
\Illuminate\Validation\ValidationException::class,
5863
JsonApiException::class,
59-
AuthenticationException::class,
60-
AuthorizationException::class,
6164
];
6265

6366
/**
@@ -78,7 +81,7 @@ public function report(Exception $e)
7881
*/
7982
public function render($request, \Exception $e)
8083
{
81-
if ($this->isJsonApi()) {
84+
if ($this->isJsonApi($request, $e)) {
8285
return $this->renderJsonApi($request, $e);
8386
}
8487

src/Utils/Helpers.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
namespace CloudCreativity\LaravelJsonApi\Utils;
1919

2020
use CloudCreativity\LaravelJsonApi\Exceptions\InvalidJsonException;
21+
use Illuminate\Http\Request;
22+
use Illuminate\Support\Str as IlluminateStr;
2123
use Psr\Http\Message\RequestInterface;
2224
use Psr\Http\Message\ResponseInterface;
2325

@@ -118,4 +120,15 @@ public static function doesResponseHaveBody(RequestInterface $request, ResponseI
118120
return 0 < $contentLength[0];
119121
}
120122

123+
/**
124+
* @param Request $request
125+
* @return bool
126+
*/
127+
public static function wantsJsonApi($request)
128+
{
129+
$acceptable = $request->getAcceptableContentTypes();
130+
131+
return isset($acceptable[0]) && IlluminateStr::contains($acceptable[0], 'vnd.api+json');
132+
}
133+
121134
}

tests/lib/Integration/ErrorsTest.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717

1818
namespace CloudCreativity\LaravelJsonApi\Tests\Integration;
1919

20+
use Carbon\Carbon;
2021
use CloudCreativity\LaravelJsonApi\Exceptions\DocumentRequiredException;
2122
use CloudCreativity\LaravelJsonApi\Exceptions\InvalidJsonException;
2223
use CloudCreativity\LaravelJsonApi\Exceptions\NotFoundException;
24+
use Illuminate\Foundation\Http\Exceptions\MaintenanceModeException;
25+
use Illuminate\Support\Facades\Route;
2326

2427
class ErrorsTest extends TestCase
2528
{
@@ -115,6 +118,46 @@ public function testCustomInvalidJson()
115118
$this->postJsonApi($uri, $content)->assertStatus(400)->assertExactJson($expected);
116119
}
117120

121+
/**
122+
* If the client sends a request wanting JSON API (i.e. a JSON API Accept header),
123+
* whatever error is generated by the application must be returned as a JSON API error
124+
* even if the error has not been generated from one of the configured APIs.
125+
*/
126+
public function testClientWantsJsonApiError()
127+
{
128+
$expected = [
129+
'errors' => [
130+
[
131+
'status' => '404',
132+
],
133+
],
134+
];
135+
136+
$this->postJsonApi('/api/v99/posts')
137+
->assertStatus(404)
138+
->assertHeader('Content-Type', 'application/vnd.api+json')
139+
->assertExactJson($expected);
140+
}
141+
142+
public function testMaintenanceMode()
143+
{
144+
Route::get('/test', function () {
145+
throw new MaintenanceModeException(Carbon::now()->getTimestamp(), 60, "We'll be back soon.");
146+
});
147+
148+
$this->getJsonApi('/test')
149+
->assertStatus(503)
150+
->assertHeader('Content-Type', 'application/vnd.api+json')
151+
->assertExactJson([
152+
'errors' => [
153+
[
154+
'detail' => "We'll be back soon.",
155+
'status' => '503',
156+
],
157+
],
158+
]);
159+
}
160+
118161
/**
119162
* @param $key
120163
* @return array
@@ -128,4 +171,4 @@ private function withCustomError($key)
128171

129172
return ['errors' => [$expected]];
130173
}
131-
}
174+
}

tests/lib/Unit/HelpersTest.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
namespace CloudCreativity\LaravelJsonApi\Tests\Unit;
1919

2020
use CloudCreativity\LaravelJsonApi\Exceptions\InvalidJsonException;
21+
use CloudCreativity\LaravelJsonApi\Utils\Helpers;
2122
use GuzzleHttp\Psr7\Request;
2223
use GuzzleHttp\Psr7\Response;
24+
use Illuminate\Http\Request as IlluminateRequest;
2325
use function CloudCreativity\LaravelJsonApi\http_contains_body;
2426
use function CloudCreativity\LaravelJsonApi\json_decode;
2527

@@ -119,6 +121,33 @@ public function testResponseContainsBody($expected, $method, $status, $headers =
119121
$this->assertSame($expected, http_contains_body($request, $response));
120122
}
121123

124+
/**
125+
* @return array
126+
*/
127+
public function wantsJsonApiProvider()
128+
{
129+
return [
130+
['application/vnd.api+json', true],
131+
['application/json', false],
132+
['text/html', false],
133+
];
134+
}
135+
136+
/**
137+
* @param $accept
138+
* @param $expected
139+
* @dataProvider wantsJsonApiProvider
140+
*/
141+
public function testWantsJsonApi($accept, $expected)
142+
{
143+
$request = new IlluminateRequest();
144+
$request->headers->set('Accept', $accept);
145+
146+
$request->wantsJson();
147+
148+
$this->assertSame($expected, Helpers::wantsJsonApi($request));
149+
}
150+
122151
/**
123152
* @param InvalidJsonException $ex
124153
*/

0 commit comments

Comments
 (0)
0