ThinkPHP6 路由闭包里我手写了 `return json()`,结果生产环境直接抛了 500 却看不到任何日志
上周给一个老项目加了个临时接口,图省事直接在 `route/app.php` 里写了个闭包路由。本地测试一切正常,推到线上直接白屏,更诡异的是 `runtime/log` 底下干干净净,连条 error 都没有。
先贴我当时的"自信代码":
// 错误写法:闭包里直接 return 助手函数
Route::get('api/temp-stats', function () {
$data = Db::table('orders')->where('status', 1)->count();
return json(['count' => $data]); // ← 坑在这里
});
本地 Windows + PHP 8.1 一切正常,线上 Linux + PHP 8.0 直接 500。我第一反应是数据库连不上,但 `try-catch` 包了一圈也没触发。后来往 `index.php` 里硬写 `error_log` 才发现,问题出在 `json()` 这个助手函数的返回值类型上。
ThinkPHP6 的闭包路由,框架内部会判断返回值是不是 `Response` 对象。`json()` 助手函数确实返回 `Json` 响应对象,但这里有个细节:如果 PHP 版本或 OPcache 某些场景下,助手函数没被正确加载,或者你像我一样在闭包里手滑写成了小写的 `json` 而实际期望的是助手函数——但更大的坑是,有些环境下 `json` 可能被识别为系统内置的 JSON 扩展函数,导致返回类型完全不对。
更隐蔽的是,这个错误在某些配置下不会走进 TP 的异常捕获流程,因为路由调度层就直接崩了,所以 `runtime/log` 里看不到记录。我花了四十分钟,最后是靠在 `public/index.php` 开头加了两行才定位到:
ini_set('display_errors', '1');
error_log("Route hit: " . date('Y-m-d H:i:s'));
正确的写法其实有两种,我现在固定用第二种:
// 写法一:明确 use Response 类,避免助手函数歧义
use think\Response;
Route::get('api/temp-stats', function () {
$data = Db::table('orders')->where('status', 1)->count();
return Response::create(['count' => $data], 'json');
});
// 写法二:更干脆,直接走控制器,别在路由里写闭包业务逻辑
Route::get('api/temp-stats', 'api.Stats/temp');
我现在给自己定了条规矩:路由文件里只允许出现 `Route::` 语句,任何业务逻辑哪怕是两行查询也必须进控制器。闭包路由看起来省事儿,但调试成本、类型隐患、后期维护都是隐形的债。
另外补充一个排查技巧:如果线上 500 且日志为空,优先检查 php-fpm.conf 里的 catch_workers_output 和 php.ini 的 log_errors 路径,很多时候错误被 PHP-FPM 吞掉了,根本没走到应用层。我这次就是错误日志写进了 /var/log/php-fpm/ 而不是项目目录,两头找不着。
你们有没有遇到过这种"日志隐身"的 500?最后是怎么揪出来的?