Mercure 在 Symfony 中发不出事件主因是 JWT 签名失败或 topic 不匹配:密钥需统一且编码一致,JWT payload 必须含 publish 权限,topic 字符串须前后端完全一致,Nginx 反代需透传 Upgrade/Connection 头。

为什么 Mercure 在 Symfony 里发不出事件
绝大多数情况是 Mercure 的 JWT 签名失败或主题不匹配,不是代码没写对,而是配置没对齐。Symfony 的 MercureBundle 默认用 symfony/mercure 客户端发事件,它要求每个事件必须带合法的 JWT(含 publish 权限),且 topic 必须与订阅方一致。
常见错误现象:401 Unauthorized 或静默丢弃(无报错但前端收不到);curl 直连 Mercure Hub 返回 {"error":"invalid_jwt"};日志里出现 "Unable to verify the JWT signature"。
-
MERCURE_PUBLISHER_JWT_KEY和MERCURE_SUBSCRIBER_JWT_KEY必须用同一份密钥,别一个用base64编码、一个没编码 - JWT payload 至少要包含
{"publish": ["*"]}或精确匹配的 topic 数组,比如{"publish": ["/api/posts/{id}"]} - Symfony 项目中,
publish方法传入的 topic 字符串必须和前端new EventSource('/.well-known/mercure?topic=...')里的完全一致(包括斜杠、通配符、大写小写) - 开发时别用
http://localhost:3000订阅却让 Mercure Hub 跑在https://localhost:8000—— 浏览器会因混合内容或 CORS 拦截
publish() 方法传参不对导致事件被过滤
Symfony MercureBundle 的 HubInterface::publish() 接收两个核心参数:$update(Mercure\Update 实例)和可选的 $hubUrl。很多人直接传字符串或数组进去,结果事件根本没进 Hub。
典型错误:把 JSON 字符串当 $data 传给 Update 构造函数,或者漏掉 EventSource 订阅时用的 topic 模式(如 /api/users/{id})。
-
Update的$data必须是字符串(通常是json_encode($payload)后的结果),不能是 PHP 数组,否则 Mercure Hub 解析失败 -
$topics参数必须是字符串数组,每个元素要是完整 topic URI,例如['/api/posts/123', '/api/users/456'],不能写成'/api/posts/123'(单字符串) - 如果用了 topic 模板(如
/api/posts/{id}),确保发布的 topic 是具体值(/api/posts/123),且前端订阅时也用了相同模板(Mercure Hub 才能做模式匹配) - 调试时可用
curl -v -X POST "$MERCURE_URL/.well-known/mercure" -H "Authorization: Bearer $JWT" -F "topic=/api/test" -F "data={\"msg\":\"ok\"}"绕过 Symfony 直连验证
本地开发时 symfony server 和 Mercure Hub 的端口/协议不一致
本地跑 symfony server:start 默认是 HTTP,而 Mercure Hub 官方 Docker 镜像默认启用 HTTPS(甚至强制重定向),两者协议不一致会导致前端 EventSource 初始化失败,控制台报 Failed to construct 'EventSource': The endpoint must be HTTP or HTTPS 或直接拒绝连接。
更隐蔽的问题是:Docker 启动的 Mercure Hub 默认监听 0.0.0.0:80,但 Symfony 应用通过 localhost:8000 反向代理访问时,Hub 收到的 Origin 是 http://localhost:8000,而 Hub 的 CORS_ALLOWED_ORIGINS 如果只设了 http://127.0.0.1:8000 就会拒绝。
- Docker 启动 Mercure Hub 时,显式指定
-e ALLOW_ANONYMOUS=1和-e CORS_ALLOWED_ORIGINS="http://localhost:8000,http://127.0.0.1:8000" - 本地开发建议统一走 HTTP:改 Mercure Hub 配置,把
cert_file和key_file注释掉,并设address=:80 - Symfony 的
.env中MERCURE_URL=http://localhost:80,别写成https;同时确保前端 JS 里的EventSourceURL 也是http - 检查
symfony server是否启用了 TLS(--no-tls参数可关掉),避免浏览器因证书问题拦截
生产环境用 Nginx 反向代理 Mercure Hub 时漏配 Upgrade 头
Nginx 默认不转发 Upgrade 和 Connection 请求头,而 Mercure Hub 依赖 WebSocket 升级机制维持长连接。漏配会导致前端反复重连、EventSource 状态卡在 CONNECTING,Network 面板看到大量 200 响应但没有 event 数据流。
这不是 Symfony 的问题,是反代层配置缺失。哪怕 Symfony 日志显示 “publish success”,只要 Nginx 没透传升级头,事件就永远到不了客户端。
- Nginx location 块里必须加:
proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection "upgrade"; -
proxy_http_version必须设为1.1(Mercure Hub 要求 HTTP/1.1 才支持 upgrade) - 别在 Nginx 里加
proxy_buffering off;以外的缓冲设置——Mercure 的 SSE 流依赖 chunked transfer,缓冲会阻塞推送 - 上线前用
curl -i -N http://yourdomain/.well-known/mercure?topic=/test看响应头是否含Content-Type: text/event-stream和持续流式输出
真正卡住人的地方往往不在 PHP 代码里,而在 JWT 密钥格式、topic 字符串的一致性、或者 Nginx 少写了那两行 header。调不通先确认这三处,比翻源码快得多。










