subscriber_jwt过期后EventSource会静默断连并触发onerror,因Mercure Hub仅校验签名和结构而不解析exp字段,实际过期控制须在PHP签发时设exp且配合Caddyfile的subscribers_timeout≤token有效期,前端需基于时间主动预刷新token。

subscriber_jwt过期后前端收不到推送
订阅令牌(subscriber_jwt)过期后,EventSource 连接会立即断开,浏览器控制台通常不报错,但 onerror 会被触发,且后续重连请求返回 401 —— 这不是网络问题,是 Mercure Hub 主动拒绝了过期凭证。
FrankenPHP 的 Mercure 模块不解析 JWT 中的 exp 字段,它只校验签名和 mercure.subscribe 结构。所以“过期”实际发生在你签发 token 的 PHP 侧:如果 payload 里写了 "exp": 1728752400,但没在服务端做校验,Hub 依然放行;反之,如果你依赖前端 JS 逻辑判断过期再换 token,就不可靠——连接可能已在后台静默断开。
- 必须在 PHP 签发时写入
exp,并确保该时间戳早于用户预期离开时间(如设为 120 秒后) - 前端不能等
onerror再取新 token:EventSource 断连后重试有延迟,且浏览器可能直接放弃(尤其在页面隐藏时) - 推荐在每次成功收到消息后,检查当前时间是否已接近
exp,提前触发刷新(例如剩余 /api/mercure-token)
如何安全刷新 subscriber_jwt 而不丢消息
直接 eventSource.close() + 新建实例会导致窗口期丢失事件。真正可行的方式是“无缝续订”:在旧连接仍活跃时,用新 token 发起第二个 EventSource,等它 ready 后再关旧的。
但这要求后端支持“同一用户多连接”,而 FrankenPHP Mercure 默认允许——只要 JWT 的 mercure.subscribe 相同,多个连接可同时接收相同 topic 的事件。
立即学习“PHP免费学习笔记(深入)”;
- 前端维护两个
EventSource实例:esCurrent和esNext - 当检测到即将过期(比如
payload.exp - Date.now() ),发起 fetch 获取新 token,创建 <code>esNext esNext.onopen = () => { esCurrent.close(); esCurrent = esNext; }- 旧连接关闭前仍能收消息,新连接 ready 后才切换,无事件空窗
Caddyfile 配置必须配合短期 token
光靠 PHP 签短 token 不够,Caddyfile 若没设 subscribers_timeout,Hub 会维持无效连接数分钟,造成资源堆积,甚至掩盖真实过期行为。
例如用户页面关闭后 token 已过期,但 Hub 还在等它重连——此时你看到的“收不到推送”其实是连接未清理,而非鉴权失败。
- 务必在
mercure块中显式配置:subscribers_timeout 60(建议 ≤ token 有效期) - 避免使用默认值(可能长达 300 秒),否则过期后连接仍挂起,干扰调试
- 搭配
publishers_timeout 30,防止发布端长连接滞留 - 不要依赖
window.onbeforeunload发送注销请求——该事件不可靠,且 Mercure 协议无注销语义
为什么不能用 fastcgi_finish_request 模拟后台续签
有些开发者试图在主响应结束前调用 fastcgi_finish_request(),想“后台异步刷新 token 并推送给前端”。这在 FrankenPHP 下完全无效:
-
fastcgi_finish_request()函数根本不存在 —— FrankenPHP 不是 FastCGI,调用会直接抛出Fatal error: Call to undefined function fastcgi_finish_request() - 即使伪造 flush 行为,也只影响当前 HTML 响应流,对已建立的 EventSource 长连接毫无作用
- Mercure 订阅是独立 HTTP GET 连接,生命周期与主页面请求解耦,无法被主响应的 flush 控制
真正可控的刷新时机只有两个:PHP 签发时控制 exp,以及前端基于时间主动轮询新 token —— 其他任何“后台干预”思路都会落空。



















