必须在 handler::render() 中判断请求类型,用 $request->expectsjson() || $request->is('api/*') 准确区分 ajax 与页面请求;中间件无法捕获路由匹配前及视图渲染阶段异常;需显式处理 modelnotfoundexception、validationexception 等,未覆盖异常须调用 parent::render() 保障日志和监控。

必须在 App\Exceptions\Handler::render() 里做判断,中间件、控制器或全局 try/catch 都无法覆盖路由匹配前或视图渲染阶段的异常。
怎么准确判断是 AJAX 还是普通页面请求
别依赖手动传参、自定义 header 或路由前缀硬编码。Laravel 提供两个语义明确、行为稳定的方法:
-
$request->expectsJson():检查Accept: application/json或X-Requested-With: XMLHttpRequest,Axios/Fetch/jQuery 默认带上,覆盖绝大多数前端场景 -
$request->is('api/*'):只匹配 URL 路径前缀,适合 API 全部走/api/xxx的项目,但不识别子域名(如v1.api.example.com)或版本路径(如/api/v1/users)
推荐组合使用:if ($request->expectsJson() || $request->is('api/*')),兼顾灵活性和向后兼容性。
为什么不能在中间件里统一转 JSON 响应
中间件执行时机在路由匹配之后、控制器运行之前,它无法捕获以下关键异常:
- 路由未匹配时抛出的
Symfony\Component\HttpKernel\Exception\NotFoundHttpException(404) - CSRF Token 失效触发的
TokenMismatchException - 视图渲染失败(如 Blade 模板语法错误、
@include找不到文件) - 框架底层初始化阶段的异常(如服务容器绑定失败)
这些异常根本不会走到中间件,直接由 Handler::render() 接管——所以中间件兜底是无效的。
常见异常类型必须单独处理,不能全交给 parent::render()
直接 return parent::render($request, $exception) 在 API 请求下会暴露堆栈(调试模式)或返回空白 500(生产环境),必须提前拦截:
-
ModelNotFoundException:API 返回response()->json(['message' => 'Not found'], 404),网页返回response()->view('errors.404') -
ValidationException:API 返回response()->json(['errors' => $exception->errors()], 422),网页建议重定向回表单页 -
AuthorizationException:统一返回 403,API 和网页都用 JSON 格式,避免前端解析 HTML 错误页
注意:所有未被显式处理的异常,最后一定要 return parent::render($request, $exception),否则 Telescope 日志、Sentry 上报、默认 500 页面都会失效。
容易踩的坑:宽泛 instanceof Exception 判断
写成 if ($exception instanceof Exception) 看似“兜底”,实则灾难:
- 会吞掉
NotFoundHttpException、MethodNotAllowedHttpException等框架内置异常,导致自定义resources/views/errors/404.blade.php不生效 - 绕过
parent::render()后,report()方法不再自动调用,异常不上报 Sentry,日志断档 - 若在
response()->view()中指定的 Blade 文件不存在,会触发二次异常,陷入无限递归崩溃
真正该做的,是按需新建具体异常类(如 InsufficientBalanceException),并在 render() 中精准 instanceof 判断——既清晰又安全。











