withRouting( web: __DIR__.'/../routes/web.php', api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { // Staging/production sit behind a reverse proxy/load balancer that // terminates SSL — without this, Laravel never sees the original // request as HTTPS, so it generates http:// asset URLs, which // browsers then block as mixed content on the https:// page (e.g. // Filament's file-upload.js failing to load, breaking that field's // JS-enhanced dropzone). Trusting '*' is the standard Laravel // pattern when the proxy's IP isn't fixed/known in advance. $middleware->trustProxies( at: '*', headers: SymfonyRequest::HEADER_X_FORWARDED_FOR | SymfonyRequest::HEADER_X_FORWARDED_HOST | SymfonyRequest::HEADER_X_FORWARDED_PORT | SymfonyRequest::HEADER_X_FORWARDED_PROTO | SymfonyRequest::HEADER_X_FORWARDED_AWS_ELB, ); $middleware->alias([ 'fastapi.agent' => EnsureFastApiAgent::class, 'api.auth' => AuthenticateSanctumOrFastApiJwt::class, ]); }) ->withExceptions(function (Exceptions $exceptions): void { // api/* always gets a JSON error envelope regardless of the // client's Accept header (T6.3) — module exceptions that define // their own render() (app-modules/*/src/Exceptions) still win, // since Laravel checks those before these fallback callbacks. $exceptions->shouldRenderJsonWhen(fn (Request $request, Throwable $e) => $request->is('api/*') || $request->expectsJson()); $exceptions->render(function (AuthenticationException $e, Request $request) { if ($request->is('api/*')) { return response()->json(['message' => 'Unauthenticated.'], 401); } }); $exceptions->render(function (AuthorizationException $e, Request $request) { if ($request->is('api/*')) { return response()->json(['message' => $e->getMessage() ?: 'This action is unauthorized.'], 403); } }); $exceptions->render(function (ModelNotFoundException $e, Request $request) { if ($request->is('api/*')) { return response()->json(['message' => 'The requested resource was not found.'], 404); } }); $exceptions->render(function (NotFoundHttpException $e, Request $request) { if ($request->is('api/*')) { return response()->json(['message' => 'The requested resource was not found.'], 404); } }); $exceptions->render(function (MethodNotAllowedHttpException $e, Request $request) { if ($request->is('api/*')) { return response()->json(['message' => 'This method is not allowed for the requested route.'], 405); } }); $exceptions->render(function (TooManyRequestsHttpException $e, Request $request) { if ($request->is('api/*')) { return response()->json(['message' => 'Too many requests.'], 429); } }); // Last-resort fallback: anything reaching here on api/* is an // exception with no render() of its own and no more specific // handler above — never let it leak a raw trace or fall through to // a bare, unenveloped 500 (T6.3). ValidationException/ // HttpResponseException are excluded — Laravel's default handling // of those (after renderable callbacks run) already produces the // right JSON envelope, this fallback would only get in the way. $exceptions->render(function (Throwable $e, Request $request) { if (! $request->is('api/*') || $e instanceof HttpExceptionInterface || $e instanceof ValidationException || $e instanceof HttpResponseException) { return null; } return response()->json([ 'message' => app()->hasDebugModeEnabled() ? $e->getMessage() : 'Server Error', ], 500); }); })->create();