根本原因是Laravel未识别API请求,需通过Accept头或$request->expectsJson()判断并返回JSON;浏览器直输URL默认带text/html头,故返回HTML错误页。

API 请求抛出异常时返回 HTML 页面,而不是 JSON,根本原因不是代码写错了,而是 Laravel 没识别出这是个 API 请求 —— 它默认按 Web 场景处理,除非你明确告诉它“这是 JSON 请求”。
为什么 APP_DEBUG=true 时还是返回 HTML 错误页
即使开启调试模式,Laravel 仍会根据请求头中的 Accept 字段决定响应格式。浏览器地址栏直输 URL 默认带 Accept: text/html,哪怕你访问的是 /api/users,框架也认为你在“看页面”,于是渲染 500.blade.php 或堆栈追踪页。
- 用
curl -H "Accept: application/json" http://localhost:8000/api/test测试,能立刻看到 JSON 响应 - Postman / axios 默认发送
Accept: application/json,所以通常没问题 - 前端 fetch 若没显式设置
headers: {'Accept': 'application/json'},也会退化为 HTML 响应 -
APP_DEBUG=false时,未匹配到$request->expectsJson()判断的自定义render()方法,会直接 fallback 到 500 HTML 页
在 Handler::render() 中正确识别并返回 JSON
不要删掉父类逻辑,也不要无条件 return JSON —— 必须先判断是否为预期的 API 请求上下文,再接管响应。否则 Web 路由(比如后台管理页)的异常也会被强制 JSON 化,导致页面白屏。
- 检查
$request->expectsJson():它内部会判断Accept头是否包含application/json,比手动解析头更可靠 - 对
ValidationException、AuthenticationException、AuthorizationException这三类高频异常优先特殊处理,它们自带语义,可复用状态码和结构 - 其他
Exception基类兜底时,统一用500状态码 + 简洁 message,避免泄露敏感信息(如APP_DEBUG=false时不应输出 trace)
public function render($request, Throwable $exception)
{
if ($exception instanceof ValidationException && $request->expectsJson()) {
return response()->json([
'code' => 422,
'message' => 'Validation failed',
'errors' => $exception->errors(),
], 422);
}
if ($exception instanceof AuthenticationException && $request->expectsJson()) {
return response()->json(['message' => 'Unauthenticated.'], 401);
}
if ($exception instanceof AuthorizationException && $request->expectsJson()) {
return response()->json(['message' => 'Unauthorized.'], 403);
}
if ($request->expectsJson()) {
return response()->json([
'code' => 500,
'message' => 'Something went wrong.',
], 500);
}
return parent::render($request, $exception);
}
自定义异常类的 render() 方法为何不生效
很多人写完自定义异常类(比如 App\Exceptions\BusinessException),在控制器里 throw new BusinessException(),却始终看到 HTML 响应——问题不在异常类本身,而在于你没把它交给 Laravel 的异常处理器统一调度。
- 必须抛出异常(
throw new BusinessException()),而不是return new BusinessException();后者只是返回一个对象实例,不会触发Handler::render() - 自定义异常类的
render()方法只有在Handler::render()中显式调用时才起作用,Laravel 不会自动发现并调用它 - 推荐做法:在
Handler::render()里加一行if ($exception instanceof BusinessException) { ... },保持控制权集中 - 如果真想让自定义异常自带渲染能力,需确保
Handler::render()最后 fallback 前已覆盖该类型,且不要漏掉$request->expectsJson()判断
前后端分离项目最常漏掉的两个硬性前提
所有优雅处理的前提,是项目已明确划清 Web 和 API 的边界。否则无论怎么重写 Handler,都会在某些路径下失效。
-
api.php路由必须使用Route::middleware('api')(而非web),因为api中间件组默认启用StartSession关闭、VerifyCsrfToken跳过等关键配置,影响异常捕获链 -
config/auth.php中的defaults.guard对 API 路由无意义,但guards.api的driver(如sanctum)必须正确定义,否则AuthenticationException根本不会被抛出,而是降级为RouteNotFoundException或 302 重定向
真正卡住人的,往往不是怎么写异常类,而是没意识到 Laravel 的异常流向严重依赖中间件组、请求头、认证守卫这三层联动。少一个,JSON 就会悄无声息地变回 HTML。











