feat(errors): add detailed error documentation pages
- Implemented views for error codes 400, 404, 422, 429, 500, and 503. - Updated error handling to use numeric codes instead of enum. - Enhanced error messages and resolution steps for better user guidance.
This commit is contained in:
parent
699cdf7f8e
commit
d9329aca92
13 changed files with 71 additions and 63 deletions
|
|
@ -15,4 +15,8 @@ return function (Middleware $middleware): void {
|
|||
|
||||
$middleware->alias([
|
||||
]);
|
||||
|
||||
$middleware->preventRequestsDuringMaintenance(except: [
|
||||
'errors/*',
|
||||
]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,4 +7,6 @@ use Presentation\Web\Http\Controllers\ErrorDocumentationController;
|
|||
use Presentation\Web\Http\Controllers\HomeController;
|
||||
|
||||
Route::get('/', HomeController::class)->name('home');
|
||||
Route::get('/errors/{code}', ErrorDocumentationController::class)->name('errors.doc');
|
||||
Route::get('/errors/{code}', ErrorDocumentationController::class)
|
||||
->whereNumber('code')
|
||||
->name('errors.doc');
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Exceptions;
|
||||
|
||||
use Shared\Enums\BaseEnum;
|
||||
|
||||
enum ApiErrorCode: string
|
||||
{
|
||||
/** @use BaseEnum<string> */
|
||||
use BaseEnum;
|
||||
|
||||
case NotFound = 'not-found';
|
||||
case ValidationError = 'validation-error';
|
||||
case BadRequest = 'bad-request';
|
||||
case TooManyRequests = 'too-many-requests';
|
||||
case InternalError = 'internal-error';
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::NotFound => 'Resource Not Found',
|
||||
self::ValidationError => 'Unprocessable Entity',
|
||||
self::BadRequest => 'Bad Request',
|
||||
self::TooManyRequests => 'Too Many Requests',
|
||||
self::InternalError => 'Internal Server Error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,9 @@ use Illuminate\Http\Request;
|
|||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Http\Exceptions\ThrottleRequestsException;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use League\Uri\Http;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
use Throwable;
|
||||
|
||||
final readonly class ApiExceptionRenderer
|
||||
|
|
@ -22,16 +25,16 @@ final readonly class ApiExceptionRenderer
|
|||
|
||||
return match (true) {
|
||||
$e instanceof NotFoundException => response()->json([
|
||||
'type' => (string) $baseUri->withPath('/errors/' . ApiErrorCode::NotFound->value),
|
||||
'title' => ApiErrorCode::NotFound->title(),
|
||||
'type' => (string) $baseUri->withPath('/errors/404'),
|
||||
'title' => 'Resource Not Found',
|
||||
'status' => 404,
|
||||
'detail' => $e->getMessage(),
|
||||
'instance' => $request->getPathInfo(),
|
||||
], 404, ['Content-Type' => self::CONTENT_TYPE]),
|
||||
|
||||
$e instanceof ValidationException => response()->json([
|
||||
'type' => (string) $baseUri->withPath('/errors/' . ApiErrorCode::ValidationError->value),
|
||||
'title' => ApiErrorCode::ValidationError->title(),
|
||||
'type' => (string) $baseUri->withPath('/errors/400'),
|
||||
'title' => 'Unprocessable Entity',
|
||||
'status' => 400,
|
||||
'detail' => $e->getMessage(),
|
||||
'invalidParams' => array_values(array_map(
|
||||
|
|
@ -57,8 +60,8 @@ final readonly class ApiExceptionRenderer
|
|||
|
||||
|
||||
$e instanceof \DomainException => response()->json([
|
||||
'type' => (string) $baseUri->withPath('/errors/' . ApiErrorCode::ValidationError->value),
|
||||
'title' => ApiErrorCode::ValidationError->title(),
|
||||
'type' => (string) $baseUri->withPath('/errors/422'),
|
||||
'title' => 'Unprocessable Entity',
|
||||
'status' => 422,
|
||||
'detail' => $e->getMessage(),
|
||||
'instance' => $request->getPathInfo(),
|
||||
|
|
@ -71,20 +74,38 @@ final readonly class ApiExceptionRenderer
|
|||
], 422, ['Content-Type' => self::CONTENT_TYPE]),
|
||||
|
||||
$e instanceof ThrottleRequestsException => response()->json([
|
||||
'type' => (string) $baseUri->withPath('/errors/' . ApiErrorCode::TooManyRequests->value),
|
||||
'title' => ApiErrorCode::TooManyRequests->title(),
|
||||
'type' => (string) $baseUri->withPath('/errors/429'),
|
||||
'title' => 'Too Many Requests',
|
||||
'status' => 429,
|
||||
'detail' => $e->getMessage(),
|
||||
'instance' => $request->getPathInfo(),
|
||||
], 429, array_merge(['Content-Type' => self::CONTENT_TYPE], $e->getHeaders())),
|
||||
|
||||
default => response()->json([
|
||||
'type' => (string) $baseUri->withPath('/errors/' . ApiErrorCode::InternalError->value),
|
||||
'title' => ApiErrorCode::InternalError->title(),
|
||||
'status' => 500,
|
||||
'detail' => Config::boolean('app.debug') ? $e->getMessage() : 'An unexpected error occurred.',
|
||||
'instance' => $request->getPathInfo(),
|
||||
], 500, ['Content-Type' => self::CONTENT_TYPE]),
|
||||
default => $this->renderDefault($e, $request, $baseUri),
|
||||
};
|
||||
}
|
||||
|
||||
private function renderDefault(Throwable $e, Request $request, Http $baseUri): JsonResponse
|
||||
{
|
||||
$status = $e instanceof HttpExceptionInterface ? $e->getStatusCode() : 500;
|
||||
$title = Response::$statusTexts[$status] ?? 'Internal Server Error';
|
||||
|
||||
$detail = $title;
|
||||
if ($e instanceof HttpExceptionInterface && $e->getMessage() !== '') {
|
||||
$detail = $e->getMessage();
|
||||
} elseif (Config::boolean('app.debug')) {
|
||||
$detail = $e->getMessage() ?: $title;
|
||||
}
|
||||
|
||||
$headers = $e instanceof HttpExceptionInterface ? $e->getHeaders() : [];
|
||||
$headers['Content-Type'] = self::CONTENT_TYPE;
|
||||
|
||||
return response()->json([
|
||||
'type' => (string) $baseUri->withPath('/errors/' . $status),
|
||||
'title' => $title,
|
||||
'status' => $status,
|
||||
'detail' => $detail,
|
||||
'instance' => $request->getPathInfo(),
|
||||
], $status, $headers);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,23 +5,28 @@ declare(strict_types=1);
|
|||
namespace Presentation\Web\Http\Controllers;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Presentation\Api\V1\Exceptions\ApiErrorCode;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class ErrorDocumentationController
|
||||
{
|
||||
public function __invoke(ApiErrorCode $code): View
|
||||
public function __invoke(int $code): View
|
||||
{
|
||||
$viewName = match ($code) {
|
||||
ApiErrorCode::NotFound => 'errors.docs.not-found',
|
||||
ApiErrorCode::ValidationError => 'errors.docs.validation-error',
|
||||
ApiErrorCode::BadRequest => 'errors.docs.bad-request',
|
||||
ApiErrorCode::TooManyRequests => 'errors.docs.too-many-requests',
|
||||
ApiErrorCode::InternalError => 'errors.docs.internal-error',
|
||||
};
|
||||
if ($code <= 0) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$viewName = 'errors.docs.' . $code;
|
||||
|
||||
if (!view()->exists($viewName)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$title = Response::$statusTexts[$code] ?? 'Unknown Error';
|
||||
|
||||
return view($viewName, [
|
||||
'code' => $code,
|
||||
'title' => $code->title(),
|
||||
'title' => $title,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
@extends('errors.docs.layout')
|
||||
|
||||
@section('status', '503')
|
||||
|
||||
@section('doc-content')
|
||||
<p>Le service est temporairement indisponible, généralement pour des raisons de maintenance.</p>
|
||||
<p>Veuillez réessayer ultérieurement.</p>
|
||||
@endsection
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
</head>
|
||||
<body>
|
||||
<span class="badge badge-@yield('status', '500')">HTTP @yield('status', '500')</span>
|
||||
<h1>{{ $title }} ({{ $code->value }})</h1>
|
||||
<h1>{{ $title }} ({{ $code }})</h1>
|
||||
|
||||
@yield('doc-content')
|
||||
|
||||
|
|
|
|||
|
|
@ -4,22 +4,20 @@ declare(strict_types=1);
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Presentation\Api\V1\Exceptions\ApiErrorCode;
|
||||
|
||||
describe('Error Documentation Pages', function (): void {
|
||||
it('renders the succinct documentation view for valid error codes', function (): void {
|
||||
// GIVEN
|
||||
/** @var \Tests\TestCase $test */
|
||||
$test = $this;
|
||||
$code = ApiErrorCode::NotFound;
|
||||
$code = 404;
|
||||
|
||||
// WHEN
|
||||
$response = $test->get("/errors/{$code->value}");
|
||||
$response = $test->get("/errors/{$code}");
|
||||
|
||||
// THEN
|
||||
$response->assertStatus(200);
|
||||
$response->assertSee('Resource Not Found');
|
||||
$response->assertSee('not-found');
|
||||
$response->assertSee('Not Found');
|
||||
$response->assertSee('404');
|
||||
});
|
||||
|
||||
it('returns a 404 for an invalid error code slug', function (): void {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue