EventSource 连接成功但无消息,主因是主题 URL 字节级不一致;需确保前后端 topic 完全相同,检查 Caddyfile mercure 配置、JWT 密钥一致性、publish 字段格式及通配符匹配规则。

EventSource 连接成功但 no message 事件触发
连接建立成功(onopen 触发)、控制台无报错、HTTP 状态码是 200,但 onmessage 死活不执行——这说明 Mercure Hub 接收了订阅,也确认了 JWT 合法性,但后续发布环节断在了中间。最常见原因是主题 URL 字节级不一致。
检查点如下:
- 前端订阅用的
topic参数值(如https://api.example.com/notifications)必须和后端POST到/.well-known/mercure时 payload 中mercure.publish数组里的字符串**完全相同**:协议、大小写、尾部斜杠、编码都不能差一个字节 - 用
curl -v "https://localhost:8080/.well-known/mercure?topic=https://api.example.com/notifications"手动连一次,观察响应头是否含Content-Type: text/event-stream且连接保持打开;再另起终端发一条消息,看这个curl进程是否立刻输出data: ... - 若手动
curl能收到,但浏览器收不到,大概率是浏览器对同一域名的 EventSource 并发数限制(Chrome 默认 6 个),开太多标签页会排队或静默失败
publisher_token 签发失败或权限不足
后端调用 mercure_publish() 或 cURL 发布时返回 401 或 403,或者干脆没返回(超时),说明 Mercure Hub 拒绝了发布请求。它只认 publisher_jwt 签发的 token,且 payload 必须含 mercure.publish 字段。
实操要点:
立即学习“PHP免费学习笔记(深入)”;
- 确保 PHP 生成 token 时用的密钥和 Caddyfile 中
publisher_jwt的值**逐字节一致**:包括换行、空格、base64 编码与否。建议统一用base64_decode(getenv('MERCURE_JWT_KEY'))传入JWT::encode() - payload 至少为:
{"mercure": {"publish": ["https://api.example.com/notifications"]}}—— 注意字段名是publish(不是subscribe),类型必须是字符串数组,URI 必须绝对且合法 - 不要在 HTTP 响应中同步发布:
curl_exec()阻塞会导致 TTFB 拉长,推荐扔进异步队列(如 Symfony Messenger + Redis transport)
Caddyfile 中 mercure 块未启用或配置被覆盖
FrankenPHP 的 Mercure 支持由 Caddy 的 mercure 指令块驱动。如果整个块被注释、拼错、或放在错误作用域(比如写在 reverse_proxy 块里),Mercure Hub 根本不会加载,所有 /.well-known/mercure 请求都会 404,而不是 401 或静默丢弃。
快速验证方法:
- 运行
frankenphp run --config ./Caddyfile --debug,观察启动日志是否出现mercure: enabled字样 - 访问
http://localhost:8080/.well-known/mercure(不带topic参数),正常应返回 JSON 描述信息;若 404,说明mercure指令根本没生效 - 确认
mercure块在 Caddyfile 中位于正确的 site 块内,且没有被其他route或handle规则提前拦截或重写路径
主题 URL 匹配逻辑被忽略或误用通配符
Mercure 主题匹配是严格前缀匹配,不是正则。你可能以为 https://api.example.com/* 能匹配所有子路径,但实际只匹配 https://api.example.com/ 开头的 URI,且通配符仅允许出现在末尾。
典型陷阱:
-
"https://api.example.com/posts/*"✅ 可匹配https://api.example.com/posts/123 -
"https://api.example.com/*/posts"❌ 不支持,会被 Mercure 忽略 -
"https://api.example.com/posts"和"https://api.example.com/posts/"是两个不同 topic,后者末尾多一个/就不会被前者订阅到 - 开发时用
localhost但前端发的是http://localhost:8080/...,而 Caddyfile 中subscriber_jwt对应的 domain 是https://localhost,协议+端口不一致也会导致匹配失败
curl 直连订阅端点并手动发布,把“客户端→Hub→发布”三段拆开验证。



















