FrankenPHP默认调用fastcgi_finish_request()导致SSE卡住,因其提前发送响应并关闭连接,使后续echo/flush无效;需设FRANKENPHP_SKIP_FINISH_REQUEST=1禁用,并配合ob_end_clean()、正确格式输出及超时控制。

这是因为 FrankenPHP 默认启用 fastcgi_finish_request(),导致响应头和内容被提前发送,而 PHP 脚本仍在执行——浏览器收不到完整数据流,EventSource 就一直卡在“连接中”,直到脚本真正结束才一次性刷出所有消息。
为什么 fastcgi_finish_request() 会让 SSE 卡住
FrankenPHP(尤其是搭配 Caddy 使用时)默认在 FastCGI 层调用 fastcgi_finish_request(),目的是让 Web 服务器尽快释放连接、提升吞吐。但它会把当前已生成的响应头 + 响应体立即发给客户端,然后“假装”请求已完成——哪怕你的 PHP 脚本还在 while 循环里调用 echo 和 flush()。
对 EventSource 来说,这等于:连接被关了,但你还在往一个已关闭的输出流写数据。结果就是所有 data: 消息积压在缓冲区,直到 PHP 脚本超时或自然退出,才被强制 flush 出去,造成“等请求结束才显示”的假象。
- 浏览器 Network 面板看到响应状态码是 200,但
Content-Length是 0 或缺失,Transfer-Encoding: chunked也不生效 - 用
curl -N http://localhost/sse测试,同样要等到脚本结束才输出全部内容 - 日志里能看到多次
ob_flush()和flush()调用,但无实际网络发送效果
怎么禁用 fastcgi_finish_request() 让 SSE 真正实时
必须通过环境变量关闭该行为,FrankenPHP 不提供配置开关,Caddy 的 php_fastcgi 指令也无法绕过它。
立即学习“PHP免费学习笔记(深入)”;
- 启动 FrankenPHP 时加环境变量:
FRANKENPHP_SKIP_FINISH_REQUEST=1 - Docker 用户在
docker run中加:-e FRANKENPHP_SKIP_FINISH_REQUEST=1 - Caddyfile 中无法直接控制,需确保宿主机或容器启动时已注入该变量
- 验证是否生效:在 SSE 脚本开头加
error_log("FINISH_REQUEST_SKIPPED: " . (function_exists('fastcgi_finish_request') ? 'yes' : 'no'), 4);,日志里应不出现fastcgi_finish_request调用痕迹
SSE 脚本本身还要注意什么
禁用 fastcgi_finish_request() 只是前提,脚本写法不对照样卡死或 502。
- 必须显式关闭所有输出缓冲:
ob_end_clean()或while (@ob_end_flush());开头清空 - 每次推送前严格调用:
echo "data: " . json_encode($msg) . "\n\n"; ob_flush(); flush(); - 加心跳防代理断连:
if ((time() % 15) === 0) { echo ": ping\n\n"; ob_flush(); flush(); } - 不要在循环里做耗时操作(如查数据库),改用 Redis Pub/Sub 或异步队列通知状态变更
- 避免用框架中间件包装 SSE 路由——Laravel 的
StartSession中间件会尝试写 session,触发session_write_close(),间接干扰 flush 流程
最容易被忽略的是:禁用 fastcgi_finish_request() 后,PHP 进程生命周期变长,必须自己管理超时和资源释放。比如没设 set_time_limit(0),脚本可能被系统 kill;没关 MySQL 连接,连接池会快速耗尽。



















