app.php 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. <?php
  2. use App\Exceptions\Problem;
  3. use App\Http\Middleware\HandleInertiaRequests;
  4. use App\Http\Middleware\OpsToken;
  5. use App\Http\Middleware\SetLocale;
  6. use App\Http\Middleware\UserOperation;
  7. use Illuminate\Auth\AuthenticationException;
  8. use Illuminate\Foundation\Application;
  9. use Illuminate\Foundation\Configuration\Exceptions;
  10. use Illuminate\Foundation\Configuration\Middleware;
  11. use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
  12. use Illuminate\Http\Request;
  13. use Illuminate\Validation\ValidationException;
  14. use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
  15. return Application::configure(basePath: dirname(__DIR__))
  16. ->withRouting(
  17. web: __DIR__.'/../routes/web.php',
  18. api: __DIR__.'/../routes/api.php',
  19. commands: __DIR__.'/../routes/console.php',
  20. health: '/up',
  21. )
  22. ->withMiddleware(function (Middleware $middleware): void {
  23. $middleware->web(append: [
  24. HandleInertiaRequests::class,
  25. AddLinkHeadersForPreloadedAssets::class,
  26. ]);
  27. // API 组中间件(来自原 Laravel 8 Kernel)
  28. $middleware->api(append: [
  29. UserOperation::class,
  30. ]);
  31. // web 组中间件
  32. $middleware->web(append: [
  33. SetLocale::class,
  34. ]);
  35. $middleware->alias([
  36. 'ops.token' => OpsToken::class,
  37. ]);
  38. })
  39. ->withExceptions(function (Exceptions $exceptions): void {
  40. // api/* 一律回 JSON。默认只在客户端声明 Accept: application/json 时才回 JSON,
  41. // 否则校验失败会 302 跳首页——客户端跟随重定向就拿到一张 HTML,
  42. // 比拿到错误码更难排查。
  43. $exceptions->shouldRenderJsonWhen(
  44. fn ($request, Throwable $e): bool => $request->is('api/*') || $request->expectsJson()
  45. );
  46. /*
  47. |----------------------------------------------------------------------
  48. | v3 的错误一律 RFC 9457 Problem Details
  49. |----------------------------------------------------------------------
  50. |
  51. | 控制器只管抛异常,格式化在这里统一做。**只接管 api/v3/**:v2 的错误
  52. | 形状(Laravel 默认的 {message, errors}、以及自写的 {ok:false,…})在
  53. | dashboard-v4 下线前一个字节都不能变。
  54. |
  55. | 不需要为 ModelNotFoundException / AuthorizationException 单独注册:
  56. | Handler::prepareException() 在 render 回调之前就把它们分别转成了
  57. | NotFoundHttpException(404) 与 AccessDeniedHttpException(403),
  58. | 会被下面的兜底按状态码正确处理;给它们写处理器反而是死代码。
  59. |
  60. | AuthenticationException 则不在那张转换表里,且默认行为会尝试重定向到
  61. | login 路由,所以必须显式处理。
  62. |
  63. */
  64. $isV3 = fn (Request $request): bool => $request->is('api/v3/*');
  65. $exceptions->render(function (ValidationException $e, Request $request) use ($isV3) {
  66. if ($isV3($request)) {
  67. return Problem::response(
  68. $request, 422, 'validation-error', null,
  69. $e->getMessage(), ['errors' => $e->errors()],
  70. );
  71. }
  72. });
  73. $exceptions->render(function (AuthenticationException $e, Request $request) use ($isV3) {
  74. if ($isV3($request)) {
  75. return Problem::response($request, 401, 'unauthenticated');
  76. }
  77. });
  78. $exceptions->render(function (Throwable $e, Request $request) use ($isV3) {
  79. if (! $isV3($request)) {
  80. return null;
  81. }
  82. $status = $e instanceof HttpExceptionInterface ? $e->getStatusCode() : 500;
  83. $slug = match (true) {
  84. $status === 404 => 'not-found',
  85. $status === 403 => 'forbidden',
  86. $status >= 500 => 'server-error',
  87. default => 'request-error',
  88. };
  89. // 生产环境不泄露 5xx 的内部信息
  90. $detail = ($status < 500 || config('app.debug')) ? $e->getMessage() : null;
  91. return Problem::response($request, $status, $slug, null, $detail);
  92. });
  93. })->create();