HTML本身不负责SSE推送,仅通过EventSource接收;真正推送必须由服务端生成合法text/event-stream响应流,需严格满足5个条件:200状态码、Content-Type为text/event-stream、Cache-Control含no-cache、每条消息以\n\n结尾、连接不可提前关闭。

HTML 本身不负责 SSE 推送,只负责接收;真正的推送必须由服务端生成合法的 text/event-stream 响应流 —— 如果你写了 new EventSource('/events') 却一直 pending 或报错 net::ERR_INCOMPLETE_CHUNKED_ENCODING,问题 90% 出在服务端响应头或输出格式没写对。
服务端响应必须满足这 5 个硬性条件
SSE 不是“发个数据就行”,浏览器会严格校验响应是否符合事件流规范。缺一不可:
- HTTP 状态码必须是
200(304、204、500都会导致连接失败) -
Content-Type响应头必须为text/event-stream(拼错、大小写错误、多空格都会失效) -
Cache-Control必须含no-cache;Safari 更敏感,建议加must-revalidate或no-store - 每条消息结尾必须是两个换行符:
\n\n(哪怕只发data:也得补上) - 连接不能被提前关闭:Node.js 不能用
res.end(),Python 不能print()后直接退出,PHP 必须flush()且保持脚本运行
Node.js/Express 中最易漏的保活和清理逻辑
本地测试可能正常,但上线后频繁断连?大概率是 Nginx 或负载均衡器默认 60 秒超时切断了空闲连接。光靠前端重连解决不了根本问题:
- 服务端需定期发送注释行保活:
res.write(':keepalive\n\n')(冒号开头的行浏览器忽略,但能重置代理超时计时器) - 必须监听
req.on('close')或res.on('close'),手动清理setInterval或数据库连接,否则内存泄漏 - 不要依赖
retry:字段控制重连间隔 —— Chrome 完全不支持,仅 Firefox 解析;重连策略应由前端onerror手动控制
示例关键片段:
app.get('/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, must-revalidate',
'Connection': 'keep-alive'
});
// 发一条初始化消息(可选)
res.write('data: {"status":"connected"}\n\n');
const timer = setInterval(() => {
res.write(`data: {"time":${Date.now()}}\n\n`);
// 保活:每 30 秒发一次注释
if (Date.now() % 30000 {
clearInterval(timer);
res.end(); // 注意:这里才是安全结束
});
});
前端 EventSource 的跨域、凭证与错误兜底
很多问题表面是“收不到数据”,实际卡在请求发不出去:
- CORS 报错?服务端必须返回
Access-Control-Allow-Origin;若前端启用了withCredentials: true,该值不能为*,必须指定具体域名 - 需要带 token?原生
EventSource不支持自定义 headers,只能走 URL 参数(如/events?token=xxx)或改用fetchEventSource(支持headers和method: 'post') -
onerror不区分错误类型:网络中断、CORS 拒绝、路径 404、服务端崩溃都触发它。判断是否真连上了,看eventSource.readyState——0表示未建立,0反复触发基本就是跨域或路由问题 - 错误时别傻等自动重连:手动
es.close()+ 延迟new EventSource(...),并指数退避(如 1s → 2s → 4s),避免雪崩
Spring Boot 的 SseEmitter 超时与异常处理陷阱
Spring Boot 封装了 SseEmitter,但默认 30 秒超时、异常后不自动清理,是线上最常见的内存泄漏源头:
- 创建时显式传
0L:new SseEmitter(0L),禁用内置超时(否则 30 秒后onTimeout触发,但连接可能还在) -
onError回调里不要只打日志 —— 必须调用emitter.complete()或emitter.send(...)后再complete(),否则对象滞留 Map 中 - 使用
ConcurrentHashMap存储 emitter 时,务必在onCompletion和onTimeout里都执行map.remove(uid),否则 OOM - 推送前必须判空:
if (emitter != null && !emitter.isCompleted()),否则抛IllegalStateException
真正容易被忽略的点是:SSE 是 HTTP 长连接,不是“发完就走”的短请求。服务端一旦开始写入流,就必须持续维护连接状态、处理中断、释放资源 —— 这些逻辑比前端 onmessage 多得多,也是绝大多数 SSE 故障的根因。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











