app.php 4.5 KB

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