先把当时的情况摆出来。Node 20 用内置 fetch 读 SSE,服务端每 30 秒发一次心跳,客户端约 300 秒后固定报 TypeError: terminated,换成 undici 的 Agent 把 bodyTimeout 关掉才稳定。
这类报错背后常见的原因有几处,逐个排除就行。undici 的 bodyTimeout 默认 300 秒。少数情况下是心跳只保活连接,不重置 body 读取超时。
少数情况下是没有自定义 dispatcher。不容易想到的是,反向代理侧也有读超时。
先确认报错的时间点是否固定
node -e "const t=Date.now();fetch('http://localhost:8080/stream').catch(e=>console.log(Date.now()-t,e.message))"
再往下用 Agent 把 bodyTimeout 设为 0
node -e "const {Agent}=require('undici');const a=new Agent({bodyTimeout:0,headersTimeout:0});console.log(a)"
接着把 dispatcher 传给 fetch 再测一次
node -e "fetch('http://localhost:8080/stream',{dispatcher:new (require('undici').Agent)({bodyTimeout:0})}).then(r=>console.log(r.status))"
同步调整代理的读超时
node -e "console.log('proxy_read_timeout 3600s')"
同一份数据我导出了两份,一份留底,一份拿来试。
这类问题以前遇到过,但这次的触发条件和上次不一样。
这类改动我都写进变更记录,过两周再出问题时能对上号。
恢复的判定其实很直接。客户端能连续读取 30 分钟不中断,心跳与正文都能正常到达,不再出现 terminated。
提醒一点。流式场景下客户端和服务端都要关掉读超时,只改一侧,另一侧到时照样断。线上能稳定复现的问题都不算难,难的是只在大流量下才出现的那种。
