Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions packages/router/src/Exceptions/HttpExceptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@
use Tempest\Core\Kernel;
use Tempest\Http\GenericResponse;
use Tempest\Http\HttpRequestFailed;
use Tempest\Http\Request;
use Tempest\Http\Response;
use Tempest\Http\Responses\Json;
use Tempest\Http\Session\CsrfTokenDidNotMatch;
use Tempest\Http\Status;
use Tempest\Router\ResponseSender;
use Tempest\Support\Filesystem;
use Tempest\View\GenericView;
use Throwable;

use function Tempest\get;
use function Tempest\Support\arr;

final readonly class HttpExceptionHandler implements ExceptionHandler
{
public function __construct(
Expand Down Expand Up @@ -49,20 +54,30 @@ public function handle(Throwable $throwable): void

private function renderErrorResponse(Status $status, ?HttpRequestFailed $exception = null): Response
{
if (get(Request::class)->headers->get('Accept') === 'application/json') {
return new Json(
$this->appConfig->environment->isLocal() && $exception !== null
? [
'message' => $exception->getMessage(),
'exception' => get_class($exception),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => arr($exception->getTrace())->map(
fn ($trace) => arr($trace)->removeKeys('args')->toArray(),
)->toArray(),
] : [
'message' => static::getErrorMessage($status, $exception),
],
)->setStatus($status);
}

return new GenericResponse(
status: $status,
body: new GenericView(__DIR__ . '/HttpErrorResponse/error.view.php', [
'css' => $this->getStyleSheet(),
'status' => $status->value,
'title' => $status->description(),
'message' => $exception?->getMessage() ?: match ($status) {
Status::INTERNAL_SERVER_ERROR => 'An unexpected server error occurred',
Status::NOT_FOUND => 'This page could not be found on the server',
Status::FORBIDDEN => 'You do not have permission to access this page',
Status::UNAUTHORIZED => 'You must be authenticated in to access this page',
Status::UNPROCESSABLE_CONTENT => 'The request could not be processed due to invalid data',
default => null,
},
'message' => static::getErrorMessage($status, $exception),
]),
);
}
Expand All @@ -71,4 +86,18 @@ private function getStyleSheet(): string
{
return Filesystem\read_file(__DIR__ . '/HttpErrorResponse/style.css');
}

private static function getErrorMessage(Status $status, ?Throwable $exception = null): ?string
{
return (
$exception?->getMessage() ?: match ($status) {
Status::INTERNAL_SERVER_ERROR => 'An unexpected server error occurred',
Status::NOT_FOUND => 'This page could not be found on the server',
Status::FORBIDDEN => 'You do not have permission to access this page',
Status::UNAUTHORIZED => 'You must be authenticated in to access this page',
Status::UNPROCESSABLE_CONTENT => 'The request could not be processed due to invalid data',
default => null,
}
);
}
}
20 changes: 20 additions & 0 deletions packages/router/src/HandleRouteExceptionMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
use Tempest\Http\Request;
use Tempest\Http\Response;
use Tempest\Http\Responses\Invalid;
use Tempest\Http\Responses\Json;
use Tempest\Http\Responses\NotFound;
use Tempest\Http\Status;
use Tempest\Router\Exceptions\ConvertsToResponse;
use Tempest\Router\Exceptions\RouteBindingFailed;
use Tempest\Validation\Exceptions\ValidationFailed;
Expand Down Expand Up @@ -38,15 +40,33 @@ public function __invoke(Request $request, HttpMiddlewareCallable $next): Respon
return $this->forward($request, $next);
}

private function wantsJson(Request $request): bool
{
return $request->headers->get('Accept') === 'application/json';
}

private function forward(Request $request, HttpMiddlewareCallable $next): Response
{
try {
return $next($request);
} catch (ConvertsToResponse $convertsToResponse) {
return $convertsToResponse->toResponse();
} catch (RouteBindingFailed) {
if ($this->wantsJson($request)) {
return new NotFound([
'message' => 'The requested resource was not found.',
]);
}

return new NotFound();
} catch (ValidationFailed $validationException) {
if ($this->wantsJson($request)) {
return new Json([
'message' => $validationException->getMessage(),
'errors' => $validationException->errorMessages,
])->setStatus(Status::UNPROCESSABLE_CONTENT);
}

return new Invalid($validationException->subject, $validationException->failingRules);
}
}
Expand Down