Log::info()在中间件中看不到请求体是因为php://input只能读取一次,且ThinkPHP的Request对象在中间件执行时可能已被前置逻辑消费;应使用$request->getContent()一次性读取原始流,并配合前置+后置双中间件分别捕获入参和出参。

为什么Log::info()在中间件里看不到请求体
因为 ThinkPHP 的 Request 对象在中间件执行时,$request->input() 或 $request->post() 可能已不可读——尤其是 POST/PUT/JSON 请求,原始流(php://input)只能读取一次。中间件里直接调用会返回空数组。
- 必须在路由调度前、且在任何可能消费输入流的逻辑之前读取原始数据
-
$request->getContent()是更稳妥的选择,它封装了对php://input的一次性读取 - 若已启用
form_params或 JSON 自动解析(如think\facade\Request::bind()),再读getContent()就会为空 - 建议只在调试中间件中使用,生产环境避免频繁记录完整 body,尤其含文件或大字段时
如何在中间件中安全记录 API 的入参和出参
ThinkPHP 6+ 推荐用「前置 + 后置」双钩子中间件组合:一个在控制器执行前抓输入,一个在响应发出前抓输出。不能只靠单个中间件的 handle() 方法完成全程捕获。
- 前置中间件中:用
$request->method()判断是否为POST/PUT/PATCH,再用$request->getContent()获取原始字符串;对GET则用$request->param() - 后置中间件中:检查
$response是否为think\Response实例,调用$response->getContent()获取响应体;注意 JSON 响应可能已被编码,需提前判断是否已设置Content-Type: application/json - 别直接记录
$request->param()全量结果——它会丢失原始 JSON 结构(比如空数组变 null)、也混淆 GET/POST 混合参数来源 - 日志内容建议结构化:包含
$request->url()、$request->ip()、$request->header('user-agent')、耗时(用microtime(true)差值)
app/middleware/ApiTraceMiddleware.php 实操要点
这个中间件文件本身不复杂,但几个关键位置容易写错:
- 构造函数里不要初始化日志句柄(如
Log::channel('api')),应延迟到handle()中按需获取,避免容器未就绪时报错 - 不要在
handle()末尾return $next($request)前修改$response内容(比如加 header),否则后置中间件拿到的是被污染的响应对象 - 记录日志时用
Log::channel('trace')->info()单独配置 channel,避免和业务日志混在一起,方便后期 grep 或对接 ELK - 如果项目用了多级代理(如 Nginx → SLB → TP),
$request->ip()默认取X-Real-IP,但需确认trust_proxies配置已正确设置可信 IP 段,否则记录的可能是内网地址
public function handle($request, \Closure $next)
{
$startTime = microtime(true);
$method = $request->method();
$url = $request->url();
$input = in_array($method, ['POST', 'PUT', 'PATCH'])
? $request->getContent()
: $request->param();
<pre class='brush:php;toolbar:false;'>$response = $next($request);
$duration = round((microtime(true) - $startTime) * 1000, 2);
$output = $response->getContent();
Log::channel('trace')->info('API_TRACE', [
'url' => $url,
'method' => $method,
'input' => $input,
'output' => $output,
'duration_ms' => $duration,
'ip' => $request->ip(),
]);
return $response;}
立即学习“PHP免费学习笔记(深入)”;
JSON 接口输出日志里出现乱码或截断怎么办
不是编码问题,是 $response->getContent() 返回的是已压缩或已 chunked 的响应流,尤其开启 output_compression 或使用 Swoole 时,内容可能被提前处理过。
- 先确认是否启用了 Gzip:查看
config/app.php中'response_compress' => true,开启状态下getContent()返回的是压缩后二进制,直接 log 会乱码 - 开发环境建议关掉压缩:
'response_compress' => false,或改用$response->getData()(仅适用于 JSON 响应且未手动 setHeader) - 若控制器返回的是
json(['code'=>0]),$response->getData()能拿到原始数组;但若用了Response::create()->code(200)->content(...),则只能依赖getContent() - 日志文件本身有大小限制(如 Linux 默认 1MB 单文件),大 payload 会被截断——用
file_put_contents('php://stderr', ...)临时调试更可靠
接口参数追踪这事,核心不在“怎么记”,而在“什么时候读、从哪读、读完还能不能继续用”。很多坑都卡在流读取时机和响应生命周期上,而不是语法写错。



















