[Bug 修复] web_fetch 加载 undici 污染全局 dispatcher,导致代理环境下"模型不可用" #5118
Replies: 2 comments
|
代理环境这族坑 +1,你定位的"全局 dispatcher 被污染"和我们撞过的兄弟坑可以拼成完整地图,给后来人:
两坑叠加的判据:设了 |
|
独立复现验证了根因 1 的机制(Node 26.5.0 + undici 8.10.0,npm 包从零安装),给你补两个实验证据 + 一个版本 nuance,并交叉映射 #5124: 实验 1: const sym1 = Symbol.for('undici.globalDispatcher.1')
const sym2 = Symbol.for('undici.globalDispatcher.2')
// 初始(未触发任何 fetch、未加载 undici)
// sym1 = undefined | sym2 = undefined
await import('undici')
// sym1 = Dispatcher1Wrapper | sym2 = Agent
// getGlobalDispatcher() = Agent ← 已被 npm undici 的默认 Agent 顶替纯模块加载即写入共享符号——你说的「import 有副作用」成立,且与你修复方案的恢复点(保存/恢复这两个符号)精确对应。 实验 2:Node 内置 fetch 与 npm undici 共用同一符号命名空间 注意这里与你在 Node v24.13.1 的观测差异:v24 首次 fetch 后 对你修复方案的验证:保存/恢复 交叉映射 #5124:d3vmeh 的 |
Uh oh!
There was an error while loading. Please reload this page.
Environment
pnpm dsh web启动)curl/npm/pip均可正常走代理NODE_USE_ENV_PROXY=1,http_proxy/https_proxy已设置(指向公司代理),no_proxy覆盖内网网段与内网域名一、问题现象
在必须通过 HTTP 代理访问外网的环境下(
NODE_USE_ENV_PROXY=1+http_proxy/https_proxy),出现如下可复现的故障链:dsh web后一切正常(模型对话、web_search均可用);web_fetch抓取外部 URL 失败(报TypeError: fetch failed),之后模型就不可用了(LLM 调用不再走代理,全部失败);Ctrl+C重启dsh web后临时恢复;即:一次失败的 web 抓取会"污染"整个进程,直到重启。
二、根因分析(以下是dsh协助分析结论)
问题出在
packages/web/web-fetch-http/src/network.ts的requestPinned,由两个叠加的 bug 构成。根因 1(主因):
await import('undici')覆盖了 Node 内置 fetch 的全局 dispatcherrequestPinned通过await import('undici')动态加载 undici@8。undici@8 的lib/global.js在模块顶层会执行:它读取的是
Symbol.for('undici.globalDispatcher.2')(undici@8 自己的符号,初始为 undefined),于是安装了一个不走代理的普通Agent,并连带把Symbol.for('undici.globalDispatcher.1')(Node 内置 fetch 依赖的符号)覆盖成Dispatcher1Wrapper(Agent)。实验证据(Node v24.13.1,代理环境):
globalThis.fetch是 LLM 调用(llm-deepseekadapter)和web_search(web-search-deepseekprovider)共同的传输通道。因此 undici 一旦被加载,进程内所有后续globalThis.fetch都失去代理支持 → "模型不可用",直到进程重启重置全局状态。根因 2(次因):
requestPinned自建 Agent 不走代理原代码用
new Agent({ autoSelectFamily: true, connect: { lookup } })直连目标,完全无视代理环境变量。在代理网络里直连被防火墙丢弃,导致web_fetch本身必然失败(实测 6 秒超时后TypeError: fetch failed)。三、修复方案
全部修改在
packages/web/web-fetch-http/src/network.ts:importUndiciWithoutGlobalPollution():在import('undici')前后保存/恢复Symbol.for('undici.globalDispatcher.1'/.2')两个符号,消除模块加载副作用,使 import 成为"纯缓存填充"——Node 内置 fetch 保持其代理感知的 dispatcher。proxyEnvironment():解析http_proxy/https_proxy/NODE_*_PROXY/NO_PROXY等别名(含 Node 的NODE_HTTP(S)_PROXY,undici@8 的类本身不读这些):EnvHttpProxyAgent(与globalThis.fetch走同一条代理通道);关键代码:
四、验证结果
fetch-http.spec.tstsc -b类型检查oxlintrequestPinned抓取patents.google.com返回 200,随后全局 fetch(LLM 路径)仍 200修复前,上述两条路径在代理环境下均必然失败。
五、改动文件
packages/web/web-fetch-http/src/network.ts(+87/-7)packages/web/web-fetch-http/tests/fetch-http.spec.ts(+68)六、备注
web-fetch-http包,不影响web_search(其本身一直走globalThis.fetch,只是会被根因 1 的全局污染连带拖垮)。globalThis.fetch且依赖 Node 内置代理配置"的场景都会踩中根因All reactions