uniformjsonresponse中间件必须在response::send()前通过setsendcallback()拦截,注册时需优先级高于think\middleware\jsonresponse,并在handle()中用response::create()返回新实例,且仅处理content-type含application/json的响应。

UniformJsonResponse中间件必须在响应出口前拦截,不能靠ExceptionHandler
ExceptionHandler的render()只在生命周期末尾触发,对ValidateException、HttpException(如404)、路由未匹配等框架级异常完全无效——它们早被前置中间件处理并输出了原始HTML或空响应。真正能稳定覆盖所有响应类型的,只有在Response::send()前介入的中间件。
常见错误现象:加了ExceptionHandler却仍收到HTML错误页;日志显示中间件执行了,但前端响应没变;json([])返回后状态码变成200。
- 必须用
Response::setSendCallback()注册回调,这是ThinkPHP官方唯一支持的临界拦截点 - 回调里读
$response->getContent(),解析、封装、再用Response::create()返回新实例 - 硬编码
json_decode()会崩:控制器可能返回视图、文件流或纯文本,得先检查$response->getHeader('Content-Type')[0]是否含application/json
中间件注册顺序错一位,统一封装就失效
ThinkPHP默认有think\middleware\JsonResponse中间件,它会把控制器返回的数组自动转成JSON响应。如果你的封装中间件排在它后面,就会被覆盖——最终还是原始结构。
正确做法是在app/middleware.php中用数字键显式控制优先级:
return [
5 => \app\middleware\UniformJsonResponse::class,
10 => \think\middleware\ResponseTrace::class,
20 => \think\middleware\CheckRequestCache::class,
];
验证方式:在中间件handle()里dump(\think\Pipeline::getMiddleware()),确认你的类排在JsonResponse之前。
- 别写成字符串数组:
[\app\middleware\UniformJsonResponse::class]——这样无法指定顺序 - 别把它放最末尾,也别插在
ResponseTrace之前——否则ResponseTrace记录的仍是原始响应 - 多应用模式下,中间件类路径和命名空间要对应子应用目录,比如
app/api/middleware/UniformJsonResponse.php对应app\api\middleware\UniformJsonResponse
handle()里return数组或echo,请求直接静默失败
中间件handle()方法签名强制要求返回\think\Response实例。返回array、string、null或调用echo/die,会导致后续中间件链中断,响应头不全,前端卡死或收空包。
正确封装逻辑必须走Response::create():
public function handle($request, \Closure $next)
{
$response = $next($request);
if ($response instanceof \think\Response &&
strpos($response->getHeader('Content-Type')[0] ?? '', 'application/json') !== false) {
$data = json_decode($response->getContent(), true) ?: [];
$wrapped = ['code' => 0, 'msg' => 'ok', 'data' => $data];
$newJson = json_encode($wrapped, JSON_UNESCAPED_UNICODE);
return \think\Response::create($newJson, 'json')
->header('Content-Length', strlen($newJson));
}
return $response;
}
- 别用
return json($wrapped)——这会触发框架二次包装,可能混入HTML页脚 - 别漏掉
Content-Length头,否则Nginx或CDN可能截断响应体 - 别在中间件里
throw新异常,那会跳过当前响应流程,重新进ExceptionHandler死循环
API路由没带Accept头,统一响应照样失效
ThinkPHP 6+ 的render()和部分中间件行为依赖Accept头判断格式,但Postman、curl、fetch默认不发Accept: application/json,框架就fallback到HTML,导致你的JSON封装中间件压根不触发。
解决方案不是改客户端,而是在中间件里加路由层判断:
- 用
$request->routeIs('api.*')匹配路由分组前缀 - 或检查路径:
str_starts_with($request->path(), 'api/') - 或结合
$request->isAjax()+$request->expectsJson()双保险
最稳妥的是三者取或:if ($request->routeIs('api.*') || str_starts_with($request->path(), 'api/') || $request->expectsJson())——这样不管前端怎么调,只要走API约定,就强制走JSON封装。
容易被忽略的点:中间件里$request->path()返回的是不含域名和查询参数的纯路径,别用$request->url()去匹配,那会包含协议和host,永远不匹配。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











