Skip to content

Commit 29d4e50

Browse files
feat: add JsonHttpExceptionHandler
1 parent 756e8c5 commit 29d4e50

File tree

3 files changed

+105
-33
lines changed

3 files changed

+105
-33
lines changed

packages/core/src/DevelopmentExceptionHandler.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Tempest\Container\Container;
66
use Tempest\Http\Request;
7+
use Tempest\Router\Exceptions\JsonHttpExceptionHandler;
78
use Tempest\Router\MatchedRoute;
89
use Throwable;
910
use Whoops\Handler\HandlerInterface;
@@ -17,13 +18,19 @@
1718
public function __construct(
1819
private Container $container,
1920
private ExceptionReporter $exceptionReporter,
21+
private JsonHttpExceptionHandler $jsonHandler,
2022
) {
2123
$this->whoops = new Run();
2224
$this->whoops->pushHandler($this->createHandler());
2325
}
2426

2527
public function handle(Throwable $throwable): void
2628
{
29+
if ($this->container->get(Request::class)->headers->get('accept') === 'application/json') {
30+
$this->jsonHandler->handle($throwable);
31+
return;
32+
}
33+
2734
$this->exceptionReporter->report($throwable);
2835
$this->whoops->handleException($throwable);
2936
}

packages/router/src/Exceptions/HttpExceptionHandler.php

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use Tempest\Http\HttpRequestFailed;
1313
use Tempest\Http\Request;
1414
use Tempest\Http\Response;
15-
use Tempest\Http\Responses\Json;
1615
use Tempest\Http\Session\CsrfTokenDidNotMatch;
1716
use Tempest\Http\Status;
1817
use Tempest\Router\ResponseSender;
@@ -31,10 +30,16 @@ public function __construct(
3130
private ResponseSender $responseSender,
3231
private Container $container,
3332
private ExceptionReporter $exceptionReporter,
33+
private JsonHttpExceptionHandler $jsonHandler,
3434
) {}
3535

3636
public function handle(Throwable $throwable): void
3737
{
38+
if (get(Request::class)->headers->get('Accept') === 'application/json') {
39+
$this->jsonHandler->handle($throwable);
40+
return;
41+
}
42+
3843
try {
3944
$this->exceptionReporter->report($throwable);
4045

@@ -54,30 +59,20 @@ public function handle(Throwable $throwable): void
5459

5560
private function renderErrorResponse(Status $status, ?HttpRequestFailed $exception = null): Response
5661
{
57-
if (get(Request::class)->headers->get('Accept') === 'application/json') {
58-
return new Json(
59-
$this->appConfig->environment->isLocal() && $exception !== null
60-
? [
61-
'message' => $exception->getMessage(),
62-
'exception' => get_class($exception),
63-
'file' => $exception->getFile(),
64-
'line' => $exception->getLine(),
65-
'trace' => arr($exception->getTrace())->map(
66-
fn ($trace) => arr($trace)->removeKeys('args')->toArray(),
67-
)->toArray(),
68-
] : [
69-
'message' => static::getErrorMessage($status, $exception),
70-
],
71-
)->setStatus($status);
72-
}
73-
7462
return new GenericResponse(
7563
status: $status,
7664
body: new GenericView(__DIR__ . '/HttpErrorResponse/error.view.php', [
7765
'css' => $this->getStyleSheet(),
7866
'status' => $status->value,
7967
'title' => $status->description(),
80-
'message' => static::getErrorMessage($status, $exception),
68+
'message' => $exception?->getMessage() ?: match ($status) {
69+
Status::INTERNAL_SERVER_ERROR => 'An unexpected server error occurred',
70+
Status::NOT_FOUND => 'This page could not be found on the server',
71+
Status::FORBIDDEN => 'You do not have permission to access this page',
72+
Status::UNAUTHORIZED => 'You must be authenticated in to access this page',
73+
Status::UNPROCESSABLE_CONTENT => 'The request could not be processed due to invalid data',
74+
default => null,
75+
},
8176
]),
8277
);
8378
}
@@ -86,18 +81,4 @@ private function getStyleSheet(): string
8681
{
8782
return Filesystem\read_file(__DIR__ . '/HttpErrorResponse/style.css');
8883
}
89-
90-
private static function getErrorMessage(Status $status, ?Throwable $exception = null): ?string
91-
{
92-
return (
93-
$exception?->getMessage() ?: match ($status) {
94-
Status::INTERNAL_SERVER_ERROR => 'An unexpected server error occurred',
95-
Status::NOT_FOUND => 'This page could not be found on the server',
96-
Status::FORBIDDEN => 'You do not have permission to access this page',
97-
Status::UNAUTHORIZED => 'You must be authenticated in to access this page',
98-
Status::UNPROCESSABLE_CONTENT => 'The request could not be processed due to invalid data',
99-
default => null,
100-
}
101-
);
102-
}
10384
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace Tempest\Router\Exceptions;
4+
5+
use Tempest\Auth\Exceptions\AccessWasDenied;
6+
use Tempest\Container\Container;
7+
use Tempest\Core\AppConfig;
8+
use Tempest\Core\ExceptionHandler;
9+
use Tempest\Core\ExceptionReporter;
10+
use Tempest\Core\Kernel;
11+
use Tempest\Http\HttpRequestFailed;
12+
use Tempest\Http\Response;
13+
use Tempest\Http\Responses\Json;
14+
use Tempest\Http\Session\CsrfTokenDidNotMatch;
15+
use Tempest\Http\Status;
16+
use Tempest\Router\ResponseSender;
17+
use Throwable;
18+
19+
use function Tempest\Support\arr;
20+
21+
final readonly class JsonHttpExceptionHandler implements ExceptionHandler
22+
{
23+
public function __construct(
24+
private AppConfig $appConfig,
25+
private Kernel $kernel,
26+
private ResponseSender $responseSender,
27+
private Container $container,
28+
private ExceptionReporter $exceptionReporter,
29+
) {}
30+
31+
public function handle(Throwable $throwable): void
32+
{
33+
try {
34+
$this->exceptionReporter->report($throwable);
35+
36+
$response = match (true) {
37+
$throwable instanceof ConvertsToResponse => $throwable->toResponse(),
38+
$throwable instanceof AccessWasDenied => $this->renderErrorResponse(Status::FORBIDDEN),
39+
$throwable instanceof HttpRequestFailed => $this->renderErrorResponse($throwable->status),
40+
$throwable instanceof CsrfTokenDidNotMatch => $this->renderErrorResponse(Status::UNPROCESSABLE_CONTENT),
41+
default => $this->renderErrorResponse(
42+
Status::INTERNAL_SERVER_ERROR,
43+
$this->appConfig->environment->isLocal() ? $throwable : null,
44+
),
45+
};
46+
47+
$this->responseSender->send($response);
48+
} finally {
49+
$this->kernel->shutdown();
50+
}
51+
}
52+
53+
private function renderErrorResponse(Status $status, ?Throwable $exception = null): Response
54+
{
55+
return new Json(
56+
$this->appConfig->environment->isLocal() && $exception !== null
57+
? [
58+
'message' => $exception->getMessage(),
59+
'exception' => get_class($exception),
60+
'file' => $exception->getFile(),
61+
'line' => $exception->getLine(),
62+
'trace' => arr($exception->getTrace())->map(
63+
fn ($trace) => arr($trace)->removeKeys('args')->toArray(),
64+
)->toArray(),
65+
] : [
66+
'message' => static::getErrorMessage($status, $exception),
67+
],
68+
)->setStatus($status);
69+
}
70+
71+
private static function getErrorMessage(Status $status, ?Throwable $exception = null): ?string
72+
{
73+
return (
74+
$exception?->getMessage() ?: match ($status) {
75+
Status::INTERNAL_SERVER_ERROR => 'An unexpected server error occurred',
76+
Status::NOT_FOUND => 'This page could not be found on the server',
77+
Status::FORBIDDEN => 'You do not have permission to access this page',
78+
Status::UNAUTHORIZED => 'You must be authenticated in to access this page',
79+
Status::UNPROCESSABLE_CONTENT => 'The request could not be processed due to invalid data',
80+
default => null,
81+
}
82+
);
83+
}
84+
}

0 commit comments

Comments
 (0)