frankenphp中异常重复记录源于php异常处理链多层触发与worker进程常驻复用:框架捕获异常并记日志后,set_exception_handler仍会二次触发;error_log()与monolog混用导致乱序覆盖;x-php-exception头仅对未被捕获异常生效,是唯一可信信源。

FrankenPHP 中同一异常被重复记录,本质是「PHP 异常处理链多层触发」+「worker 进程常驻复用」共同导致的。最典型现象:一条 throw new Exception('xxx') 在日志里出现 2–3 次,分别来自 set_exception_handler、error_log()、框架 App\Exceptions\Handler 和 Caddy 的 handle_errors 块。这不是 bug,而是各层日志通道没对齐造成的冗余。
为什么 set_exception_handler 会和框架 Handler 冲突
在 FrankenPHP worker 模式下,set_exception_handler() 注册的回调只对「当前请求中未被 try/catch 捕获的异常」生效;但 Laravel、ThinkPHP 等框架会在中间件或内核层主动 catch 所有异常,并调用自己封装的 render() 方法——这个方法内部往往又调了 error_log() 或写 Monolog。结果就是:异常先被框架捕获并记一次,再因 render() 里没做防重逻辑,又被 set_exception_handler 再记一次。
- 检查你的
App\Exceptions\Handler::render()是否包含error_log($e->__toString())或类似写日志动作 - 确认
set_exception_handler()是否放在了框架初始化之后(比如bootstrap/app.php之后),否则它可能根本没注册上 - 禁用框架默认错误渲染:Laravel 可临时在
Handler::render()开头加if (app()->environment('production')) return parent::render($request, $e);,让生产环境跳过框架日志
怎么让 error_log() 不和 Monolog 重复写
如果你已接入 Monolog(比如用 Monolog\Handler\StreamHandler 写入 /var/log/app.log),就绝不能再让 PHP 原生的 error_log() 往同一个文件或路径写——因为 error_log() 是无锁、无缓冲的系统调用,而 Monolog 是带格式化、轮转、异步能力的库,两者混用必然乱序、覆盖甚至丢行。
- 入口脚本(如
public/index.php)顶部立刻执行:ini_set('error_log', '/dev/null'),彻底关闭 PHP 默认错误日志输出 - 确保
log_errors = On仍开启(用于触发set_error_handler),但输出目标已被重定向 - 所有业务日志统一走 Monolog 实例,包括
trigger_error()类警告:需配合set_error_handler()将其转为$logger->warning() - 不要在 Monolog 的 StreamHandler 路径上再配 Caddy 的
log { output file ... }—— 它们语义不同:Caddy log 是 HTTP 访问日志,Monolog 是应用行为日志,强行合并只会让排查变难
FRANKENPHP_DEBUG=1 与 X-PHP-Exception 响应头别滥用
FRANKENPHP_DEBUG=1 启动时,FrankenPHP 会在响应头注入 X-PHP-Exception,把异常类名和消息 Base64 编码后透出。这方便前端监控抓取,但若你同时开了 display_errors = On 或框架 APP_DEBUG=true,就会造成「响应体里打印堆栈 + 响应头里再塞一遍」,前端日志系统可能当成两条独立事件上报。
- 线上必须关掉
display_errors和APP_DEBUG,只保留FRANKENPHP_DEBUG=1供 header 透传 - 前端监控(如 Sentry SDK)应只解析
X-PHP-Exception头,忽略响应体内容;后端日志服务也只需监听该 header 并解码,不读响应体 - 注意:该 header 只在未被捕获的异常(即逃逸到
set_exception_handler的 Throwable)中出现;被框架catch的异常不会触发它——所以它天然和框架日志不重叠,是理想的唯一信源
真正要盯住的是「谁最先拿到异常」——要么交给框架统一处理并禁用原生 handler,要么绕过框架直接用 set_exception_handler + Monolog,但二者不可共存。worker 进程不重启,上下文就一直复用,日志通道一旦错配,重复记录就会持续发生,且很难靠 grep 日志发现根源。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











