在ThinkPHP 6中,应于响应生成后、发送前(如think\ResponseSend事件或中间件handle结尾)调用$response->getStatusCode()获取状态码;禁用http_response_code()和header正则匹配,避免Swoole下复用导致的错乱。

ThinkPHP 6 中如何获取并统计 HTTP 状态码
直接在中间件或 App::after 事件里读取响应状态码即可,Response 对象的 getStatusCode() 方法是唯一可靠入口。别试图从 header 字符串里正则匹配,也别依赖 http_response_code() —— 它在 Swoole 或多线程环境下可能返回上一个请求的残留值。
常见错误现象:在控制器里调用 http_response_code() 总是返回 200,哪怕你刚执行了 throw new HttpException(404);或者在 Swoole 模式下状态码统计明显错乱。
- 状态码必须在响应已生成、但尚未发送给客户端前读取(即
Response::send()之前) - 推荐监听
think\ResponseSend事件,这是最安全的钩子点 - 若用中间件,确保它排在所有业务中间件之后(
priority设为负数,如-100)
用中间件统计并写入日志或 Redis
中间件是最常用、最可控的统计入口。注意不要在 handle() 的开头就读状态码 —— 此时响应还没生成,$response->getStatusCode() 一定是 200。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 在
handle()结尾处读取:$response->getStatusCode() - 把统计逻辑封装成独立方法,避免污染中间件主流程
- 异步写入(如用
go协程或队列)避免阻塞响应,尤其在高并发场景下
示例代码片段:
public function handle($request, \Closure $next)
{
$response = $next($request);
$code = $response->getStatusCode();
// 记录到 Redis Hash(按天分 key)
$key = 'stat:http_code:' . date('Ymd');
$redis = app('cache')->store('redis')->getRedis();
$redis->hIncrBy($key, (string)$code, 1);
return $response;
}
为什么不能在控制器里用 response()->code() 统计
因为 response()->code(404) 只是设置状态码,并不立即生效;真正写入响应头是在 Response::send() 阶段。你在控制器里调用它之后立刻查 getStatusCode(),拿到的仍是旧值(通常是 200)。
更隐蔽的问题是:异常中断流程(比如抛出 ValidateException)会跳过控制器后续代码,导致你预设的统计逻辑根本没机会执行。
- 控制器层适合做「业务维度」标记(如
$this->log->info('order_failed', ['code' => 400])),不适合做「HTTP 层」状态码兜底统计 - 所有未被捕获的异常最终都会走到异常处理组件(
think\exception\Handle),那里才是补漏的关键位置 - 如果用了自定义异常处理器,记得在
render()方法末尾手动触发一次状态码采集
兼容 Swoole 和 FPM 的统计方案要点
Swoole 下 Response 对象会被复用,FPM 下每次请求都是全新实例 —— 这个差异会让很多“看似正常”的统计代码在线上 Swoole 环境中出错。
关键规避点:
- 绝不缓存
Response实例或其状态码到静态变量或全局数组中 - 避免使用
$_SERVER['REDIRECT_STATUS'],它在 Swoole 中不可靠,在 CLI 模式下根本不存在 - Redis 写入务必用原子操作(
hIncrBy),不要先hGet再hSet,否则并发时会丢数据 - 如果统计精度要求极高(比如要区分 401 和 403),建议在
ResponseSend事件里用app('log')写结构化日志,再由外部程序聚合分析
状态码统计本身不难,难的是在不同运行模式、不同异常路径、不同生命周期阶段都保持一致行为。最容易被忽略的,其实是 Swoole 下 Response 对象的复用机制 —— 它会让“看起来没问题”的代码在线上悄悄失效。



















