AI 对话已经在服务端逐段生成,浏览器却安静了十几秒,随后一下子显示整段内容。另一些请求能逐条收到消息,但空闲一段时间就断开。SSE 接入 CDN、反向代理或网关后,这两种故障需要分别处理:前者检查数据在哪一层被积攒,后者检查哪一段连接触发了空闲超时。
本文针对返回 text/event-stream 的 HTTP 接口。WebSocket 有自己的连接与升级机制,不适用这里的完整配置。以下命令和程序是诊断示例,不是对任何生产站点的实测记录。

确认应用发出的确实是完整事件
SSE 使用 UTF-8 文本,事件由空行结束。服务端写出一行 data: 后,如果没有再补一个空行,客户端会继续等待这条事件结束。[1]
data: 第一段内容
程序里通常写成 data: 第一段内容\n\n。浏览器原生 EventSource 解析器会按事件边界分发消息;如果客户端使用 fetch() 加自定义流解析器,还要检查它有没有先把整个响应读完,再统一更新页面。HTTP 网络数据块和 SSE 事件不保证一一对应。
可以用下面的最小 Node.js 服务隔离模型生成、数据库和业务框架。它每秒写出一条事件,共写五条,然后结束连接。flushHeaders() 负责尽早发出响应头;消息能否及时到达浏览器,还取决于后面的代理和网络。[2]
const http = require('node:http');
http.createServer((req, res) => {
if (req.url !== '/events-test') {
res.writeHead(404).end();
return;
}
res.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-store',
'X-Accel-Buffering': 'no'
});
res.flushHeaders();
let n = 0;
const timer = setInterval(() => {
res.write(`data: ${JSON.stringify({ n: ++n, at: Date.now() })}\n\n`);
if (n === 5) {
clearInterval(timer);
res.end();
}
}, 1000);
res.on('close', () => clearInterval(timer));
}).listen(8080, '127.0.0.1');这段程序默认只监听本机,适合在源站上经反向代理测试。它没有认证和业务限制,不应为了调试直接开放到公网。原生 EventSource 在连接结束后可能自动重连;测试时用 curl,或由浏览器客户端主动关闭连接。
用同一个接口对照源站与 CDN
curl 默认可能缓冲输出。检查逐条到达时间时,加上 --no-buffer,避免把终端自己的缓冲误认成 CDN 故障。[3]
curl --no-buffer --max-time 15 \ -D sse-edge-headers.txt \ https://api.example.com/events-test curl --no-buffer --max-time 15 \ --resolve api.example.com:443:203.0.113.10 \ -D sse-origin-headers.txt \ https://api.example.com/events-test
第一条走正常 DNS 和 CDN,第二条将同一主机名固定到测试源站。替换示例域名和保留地址 203.0.113.10;后者是文档示例地址。--resolve 保留 URL 中的主机名,便于使用相同的 Host 与 TLS 主机名检查路径。源站需要有匹配的证书,且允许测试机连接。[3]
源站只接受 CDN 流量时,在批准的测试环境使用同等入口,不要放开整个源站防火墙,也不要用 -k 跳过证书验证。测试文件可能包含敏感响应头,保存和分享前应做脱敏。
观察每次输出发生的时间,而不只是服务器消息里的时间戳:
| 观测结果 | 下一步检查 |
|---|---|
| 直连源站也成批输出 | 应用是否逐次写出、事件空行、框架及源站代理缓冲 |
| 直连逐条输出,经过 CDN 成批输出 | CDN 流式支持、响应缓冲、压缩与边缘处理规则 |
| curl 逐条输出,页面仍一次显示 | 前端读取和渲染逻辑、浏览器路径差异 |
| 收到消息正常,空闲后断开 | 各层读取超时、心跳、连接总时长上限 |
这里的对照用于缩小问题范围。多源站、区域路由和不同请求头可能导致两条测试走向不同应用实例,需要一并记录。
Nginx 只对流式接口关闭响应缓冲
Nginx 的 proxy_buffering 控制代理响应缓冲。关闭后,它会在收到上游数据时同步向客户端传递;上游也可以使用 X-Accel-Buffering: no 控制该行为,但管理员可通过 proxy_ignore_headers 禁用这种控制。[4]
如果 /events/ 是专门的流式路径,可以在已有代理配置上做局部调整:
location /events/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_buffering off;
proxy_cache off;
gzip off;
proxy_read_timeout 75s;
}保留现有鉴权、限流、日志、其他必要的转发头和访问限制,不要直接用示例替换生产 location。proxy_pass 未附加 URI,转发时保留请求路径;上面的 /events-test 最小程序需要对应的测试路由,不能直接拿它验证 /events/ 下的业务路径。
75s 是示例值,需要结合业务和代理链路设置。Nginx 的 proxy_read_timeout 限制两次读取上游响应数据之间的间隔,不是整个响应允许持续的总时长。[4] 因此,流式请求持续数分钟和某一段空闲超过 75 秒,是两种不同情况。
proxy_request_buffering 管的是客户端请求体,修改它通常不能解决下行 SSE 响应积攒。压缩也作为独立变量测试:临时在这个路径关闭 gzip,并检查边缘是否仍做压缩或响应转换,不要全站关闭已有优化。修改前备份配置,运行 nginx -t,通过后再按既定变更流程重载。
CDN 配置需要单独核对
源站的 X-Accel-Buffering: no 可以影响支持它的代理。不能据此推断 CDN 一定会关闭缓冲。逐项查看当前 CDN 产品对流式响应的支持,以及该路径实际匹配的配置:
动态事件接口绕过缓存,不复用包含用户信息的响应;
不执行需要等待完整响应体的页面改写或边缘程序逻辑;
分开检查压缩、响应缓冲及回源连接参数;
明确空闲超时与总连接时长限制,确认两者是否都存在。
源站返回 Cache-Control: no-store 是建议配置,仍应检查 CDN 上显式覆盖缓存行为的规则。不要只看响应里有没有某个命中标记就判断整条链路可流式传输。
测试时一次改变一个因素,并记录请求路径、响应头、客户端实际接收时间和规则版本。全站关缓存容易扩大性能影响,也会掩盖具体是哪条规则出了问题。
空闲断线用心跳验证,恢复连接靠事件编号
SSE 支持以冒号开头的注释行。对于可能较长时间没有业务数据的连接,服务端可以定时写出注释心跳,例如 : ping\n\n。规范中的作者建议提到约每 15 秒发送一条注释以应对部分旧代理;实际间隔应低于链路中最短的空闲超时,并按负载进行验证。[1]
心跳也必须及时穿过代理。如果仍被缓冲,发送频率再高也未必能保持客户端这一段连接。硬性的总连接时长上限也不会因为心跳自动消失。
原生 EventSource 支持重连,以及通过事件中的 id: 和后续请求中的 Last-Event-ID 传递恢复位置。[1] 应用需要自行保留可重放记录,定义重放范围,并处理重复事件;浏览器自动重连不会替你补齐丢失的业务数据。使用 fetch() 的自定义客户端,应自行实现相应的重连与恢复流程。
用户专属事件流还要检查认证、跨域策略和断开后的资源清理。不要用关闭鉴权或共享缓存的方式换取“能收到消息”。
验收看逐条到达和断线恢复
先保留一次变更前的结果,再用同一客户端、同一接口对照修改后表现。最小程序的五条消息应按时间分开到达;业务接口还要覆盖长空闲、正常结束、客户端主动断开与网络中断后的恢复。
通过源站代理和 CDN 分别跑测试,检查第一条事件等待时间、连续事件到达间隔、重连后的重复或丢失情况。确认目标路径的规则生效后,再验证普通页面与下载接口没有受到影响。若修改无效,回滚到备份配置,继续定位上一层,不要累积一组无法解释的超时与缓冲开关。
参考资料
[1] WHATWG HTML Living Standard:Server-sent events(事件格式、注释、重连及 Last-Event-ID)。
[2] Node.js HTTP 文档:response.flushHeaders() 与 response.write()。
[3] curl man page:--no-buffer、--resolve、--max-time。
[4] Nginx ngx_http_proxy_module:proxy_buffering、proxy_read_timeout、proxy_request_buffering。
文档查阅日期:2026 年 10 月 8 日。示例未在用户生产环境执行。