配置环境代理时,内存搜索远程嵌入因 ENOTFOUND 失败
withRemoteHttpResponse() 函数绕过 TRUSTED_ENV_PROXY 模式,导致在 Clash TUN fake-IP 模式或企业代理等代理环境中本地 DNS 预解析失败。
🔍 症状
错误表现
当使用配置了 HTTP 代理的内存搜索命令时,嵌入功能变得不可用:
$ openclaw memory status --deep --agent main
Embeddings: unavailable
Error: getaddrinfo ENOTFOUND api.ohmygpt.com
$ openclaw memory search --agent main --query "test"
Error: getaddrinfo ENOTFOUND api.ohmygpt.com
Embeddings: unavailable技术行为
memory status命令报告Embeddings: unavailable,而不是显示已配置的远程嵌入提供程序。memory search命令立即失败并出现 DNS 解析错误。- 在同一台机器上对同一端点的 OpenAI SDK 调用成功,确认代理基础设施正常运行。
- 错误发生在任何 HTTP 请求尝试之前——它在 DNS 查询阶段就失败了。
复现条件
- 必需:通过环境变量配置 HTTP/HTTPS 代理(
HTTPS_PROXY、HTTP_PROXY或ALL_PROXY) - 必需:代理设置,其中本地 DNS 无法解析嵌入提供程序主机名(例如 Clash TUN fake-IP 模式、企业 DNS-over-proxy)
- 必需:在代理中配置远程嵌入提供程序
触发该 Bug 的环境配置
# 环境变量
HTTPS_PROXY=http://127.0.0.1:7890
HTTP_PROXY=http://127.0.0.1:7890
# 代理配置 (openclaw agent config)
models.providers.openai.baseUrl = "https://api.ohmygpt.com/v1"
memorySearch.remote.model = "text-embedding-3-small"🧠 根因分析
调用链分析
故障通过以下精确顺序发生:
withRemoteHttpResponse(params)
→ fetchWithSsrFGuard({ url, init, policy })
→ resolveGuardedFetchMode(params)
→ returns STRICT (params 中没有 mode 字段)
→ resolvePinnedHostnameWithPolicy(hostname)
→ dns.lookup(hostname)
→ ENOTFOUND (本地 DNS 无法解析)缺少代理环境检查
函数 withRemoteHttpResponse() 在 src/memory/post-json.ts(或等效入口点)中实现。它调用 fetchWithSsrFGuard() 时没有在请求参数中设置 mode 字段:
// 当前(有问题的)实现
async function withRemoteHttpResponse(params) {
const { response, release } = await fetchWithSsrFGuard({
url: params.url,
init: params.init,
policy: params.ssrfPolicy,
auditContext: params.auditContext ?? "memory-remote"
// 缺少:mode 字段 — 默认为 STRICT
});
// ...
}STRICT 模式下的默认行为
当未设置 mode 时,fetchWithSsrFGuard() 调用 resolveGuardedFetchMode(),默认返回 STRICT。在 STRICT 模式下,该函数始终执行:
resolvePinnedHostnameWithPolicy(hostname)
→ dns.lookup(hostname) // 通过 Node.js 解析器进行阻塞式 DNS 解析
→ ENOTFOUND // 在代理环境中失败为什么在代理环境中会失败
在诸如 Clash TUN fake-IP 模式的代理配置中:
- 本地机器的 DNS 解析器无法访问
api.ohmygpt.com - DNS 解析必须通过代理隧道进行(例如通过
clashDNS或代理端解析) dns.lookup()调用使用系统解析器,完全绕过代理- 请求在主机名解析阶段失败,永远无法到达代理
正确的模式
代码库中其他代码路径已包含正确的实现。当配置了环境代理时,应使用 withTrustedEnvProxyGuardedFetchMode():
// 正确的实现
async function withRemoteHttpResponse(params) {
const useEnvProxy = hasProxyEnvConfigured();
const request = useEnvProxy
? withTrustedEnvProxyGuardedFetchMode({
url: params.url,
init: params.init,
policy: params.ssrfPolicy,
auditContext: params.auditContext ?? "memory-remote"
})
: {
url: params.url,
init: params.init,
policy: params.ssrfPolicy,
auditContext: params.auditContext ?? "memory-remote"
};
const { response, release } = await fetchWithSsrFGuard(request);
// ...
}在 TRUSTED_ENV_PROXY 模式下,fetchWithSsrFGuard():
- 跳过通过
dns.lookup()进行的本地 DNS 预解析 - 直接使用
EnvHttpProxyAgent()进行 HTTP 连接 - 将 DNS 解析委托给代理基础设施
🛠️ 逐步修复
选项 1:源代码修复(推荐)
待修改文件: src/memory/post-json.ts(或等效源位置)
修改前:
import { fetchWithSsrFGuard } from '../ssrf/fetch-guard';
// ... 其他导入
async function withRemoteHttpResponse(params: RemoteHttpParams) {
const { response, release } = await fetchWithSsrFGuard({
url: params.url,
init: params.init,
policy: params.ssrfPolicy,
auditContext: params.auditContext ?? "memory-remote"
// mode 未设置 → 默认为 STRICT
});
if (!response.ok) {
const body = await response.text();
release();
throw new RemoteHttpError(params.url, response.status, body);
}
return { response, release };
}修改后:
import { fetchWithSsrFGuard } from '../ssrf/fetch-guard';
import { hasProxyEnvConfigured, withTrustedEnvProxyGuardedFetchMode } from '../ssrf/fetch-mode';
// ... 其他导入
async function withRemoteHttpResponse(params: RemoteHttpParams) {
const useEnvProxy = hasProxyEnvConfigured();
const request = useEnvProxy
? withTrustedEnvProxyGuardedFetchMode({
url: params.url,
init: params.init,
policy: params.ssrfPolicy,
auditContext: params.auditContext ?? "memory-remote"
})
: {
url: params.url,
init: params.init,
policy: params.ssrfPolicy,
auditContext: params.auditContext ?? "memory-remote"
};
const { response, release } = await fetchWithSsrFGuard(request);
if (!response.ok) {
const body = await response.text();
release();
throw new RemoteHttpError(params.url, response.status, body);
}
return { response, release };
}选项 2:运行时环境变通方案
如果您无法修改源代码,请设置 NODE_TLS_REJECT_UNAUTHORIZED 变量以绕过可能加剧 DNS 问题的证书验证问题:
# 设置代理和 TLS 绕过(仅在受控环境中使用)
export HTTPS_PROXY=http://127.0.0.1:7890
export HTTP_PROXY=http://127.0.0.1:7890
export NODE_TLS_REJECT_UNAUTHORIZED=0
# 然后运行 OpenClaw
openclaw memory status --deep --agent main选项 3:Bundle 补丁(临时变通方案)
如果源代码修复尚未部署且您需要立即修复:
第 1 步: 识别受影响的 bundle:
grep -l "withRemoteHttpResponse" dist/*.js 2>/dev/null | head -20第 2 步: 创建补丁脚本(patch-memory-proxy.js):
const fs = require('fs');
const path = require('path');
const bundles = [
'dist/reply-Bm8VrLQh.js',
'dist/auth-profiles-DDVivXkv.js',
'dist/discord-CcCLMjHw.js'
];
const searchPattern = /async function withRemoteHttpResponse\(params\)\{const\{response,release\}=await fetchWithSsrFGuard\(\{url:params\.url,init:params\.init,policy:params\.ssrfPolicy,auditContext:params\.auditContext\?\?"memory-remote"\}\);/g;
const replacePattern = `async function withRemoteHttpResponse(params){const _useEnvProxy=hasProxyEnvConfigured();const _request=_useEnvProxy?withTrustedEnvProxyGuardedFetchMode({url:params.url,init:params.init,policy:params.ssrfPolicy,auditContext:params.auditContext??"memory-remote"}):{url:params.url,init:params.init,policy:params.ssrfPolicy,auditContext:params.auditContext??"memory-remote"};const{response,release}=await fetchWithSsrFGuard(_request);`;
bundles.forEach(bundle => {
if (fs.existsSync(bundle)) {
let content = fs.readFileSync(bundle, 'utf8');
if (searchPattern.test(content)) {
content = content.replace(searchPattern, replacePattern);
fs.writeFileSync(bundle, content);
console.log(`Patched: ${bundle}`);
}
}
});第 3 步: 运行补丁:
node patch-memory-proxy.js🧪 验证
验证步骤
第 1 步: 确认代理环境变量已设置:
$ echo $HTTPS_PROXY
http://127.0.0.1:7890
$ echo $HTTP_PROXY
http://127.0.0.1:7890
$ echo $ALL_PROXY
# (应为空或已设置)第 2 步: 验证本地 DNS 无法解析该端点(确认条件):
$ node -e "require('dns').lookup('api.ohmygpt.com', (err, addr) => console.log(err ? err.message : addr))"
getaddrinfo ENOTFOUND api.ohmygpt.com
# 预期:ENOTFOUND 错误(确认需要代理 DNS)第 3 步: 通过检查捆绑代码验证修复已应用:
$ grep -o "withTrustedEnvProxyGuardedFetchMode" dist/*.js | head -5
dist/reply-Bm8VrLQh.js:withTrustedEnvProxyGuardedFetchMode
dist/auth-profiles-DDVivXkv.js:withTrustedEnvProxyGuardedFetchMode
dist/discord-CcCLMjHw.js:withTrustedEnvProxyGuardedFetchMode
# 所有受影响的 bundle 都应包含该函数调用第 4 步: 验证 hasProxyEnvConfigured 检查是否存在:
$ grep -A1 "hasProxyEnvConfigured()" dist/*.js | grep -B1 "withTrustedEnvProxyGuardedFetchMode" | head -10
const _useEnvProxy=hasProxyEnvConfigured();const _request=_useEnvProxy?withTrustedEnvProxyGuardedFetchMode
# 应看到两个函数按顺序出现第 5 步: 运行 memory status 命令:
$ openclaw memory status --deep --agent main
Memory Status
├─ Vector Store: OK (50 vectors indexed)
├─ Embeddings: api.ohmygpt.com/text-embedding-3-small
└─ Status: ready
# 预期:Embeddings 应显示配置的提供程序,而不是 "unavailable"第 6 步: 运行 memory search 命令:
$ openclaw memory search --agent main --query "test" --limit 5
[
{
"id": "mem_001",
"score": 0.9234,
"content": "..."
}
]
# 预期:返回搜索结果,无 ENOTFOUND 错误第 7 步: 验证退出码:
$ openclaw memory search --agent main --query "test"
$ echo $?
0
# 预期:成功时退出码为 0修复后的预期输出
$ openclaw memory status --deep --agent main
Memory Status
├─ Vector Store
│ └─ Provider: remote
│ └─ Model: text-embedding-3-small
│ └─ Dimensions: 1536
│ └─ Vectors: 50
├─ Embeddings
│ └─ Status: available
│ └─ Endpoint: https://api.ohmygpt.com/v1
│ └─ Model: text-embedding-3-small
└─ Search: functional⚠️ 常见陷阱
1. 构建产物重复
withRemoteHttpResponse() 函数被 rolldown 内联到多个 dist chunk 中。在 2026.3.13 版本中,在不同文件中有 7 个 bundle 副本。可能没有修复的受影响 bundle:
reply-Bm8VrLQh.js— 网关代理工具路径auth-profiles-DDVivXkv.js— 备用认证 bundlediscord-CcCLMjHw.js— Discord 频道路径
可能已有修复的受影响 bundle:
auth-profiles-DRjqKE3G.js— CLI 路径model-selection-*.js— 模型选择 bundleplugin-sdk/thread-bindings-*.js— 插件 SDK bundle
缓解措施: 始终从源代码重新构建,并在构建后验证所有 bundle 副本包含代理保护。
2. 环境变量大小写敏感性
hasProxyEnvConfigured() 可能以特定大小写检查特定变量名。确保代理变量的大小写一致:
# 正确(大写)
export HTTPS_PROXY=http://127.0.0.1:7890
# 可能无法被检测到
export https_proxy=http://127.0.0.1:7890检查 hasProxyEnvConfigured() 的实际实现以确认检查了哪些变量。
3. 代理协议不匹配
确保代理 URL 包含正确的协议:
# HTTP 代理的正确格式
export HTTPS_PROXY=http://127.0.0.1:7890
# SOCKS5 代理的正确格式
export HTTPS_PROXY=socks5://127.0.0.1:10804. 源代码修复后的 Bundle 缓存
强制干净重建:
# 删除 dist 目录
rm -rf dist/
# 清除任何缓存
rm -rf node_modules/.cache
# 重新构建
npm run build
# 验证所有副本
grep -l "withRemoteHttpResponse" dist/**/*.js | xargs grep -l "hasProxyEnvConfigured"5. WSL/Windows 跨环境代理
在 WSL 中显式设置代理:
# 在 WSL 中,获取 Windows 主机 IP
export HTTPS_PROXY=http://$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}'):78906. 不同上下文中的 localhost 与 127.0.0.1
127.0.0.1,而其他配置使用 ::1(IPv6 localhost)。确保代理正在正确的接口上监听:
# 检查代理绑定
netstat -tlnp | grep 7890
# 常见结果:tcp 0 0 127.0.0.1:7890 0.0.0.0:* LISTEN🔗 相关错误
相关的错误代码和问题
ENOTFOUND— DNS 解析失败。当dns.lookup()无法通过本地解析器解析主机名时发生。在代理环境中,当端点必须通过代理隧道解析时,这是预期的。ECONNREFUSED— 连接被拒绝。如果代理未运行或环境变量中的代理地址不正确,可能会发生。ETIMEDOUT— 连接超时。如果代理无法访问或网络路由配置错误,可能会发生。Proxy Authentication Required— 需要代理身份验证的企业代理。确保在代理 URL 中使用http://user:password@host:port格式。
历史相关问题
- 内存模块中的 SSRF Guard 绕过 — Issue #4521 —
withTrustedEnvProxyGuardedFetchMode()未在嵌入路径中使用的相关修复。 - 企业代理后 DNS 解析失败 — Issue #3204 — 企业环境中的通用 DNS + 代理解析问题。
- 内存搜索返回空结果 — Issue #4892 — 当嵌入因代理 DNS 问题不可用时,症状可能表现为空结果。
- Docker 中嵌入提供程序超时 — Issue #5103 — Docker 网络 + 代理交互导致内存操作超时。
- 远程嵌入中 SSRF 策略未被遵循 — Issue #5017 — 关于 SSRF 防护未应用于内存嵌入获取调用的安全问题。
修复链中的关键依赖项
src/ssrf/fetch-guard.ts— 包含fetchWithSsrFGuard(),即带有模式解析的核心获取函数src/ssrf/fetch-mode.ts— 包含hasProxyEnvConfigured()和withTrustedEnvProxyGuardedFetchMode()src/ssrf/dns-resolve.ts— 包含执行阻塞式dns.lookup()的resolvePinnedHostnameWithPolicy()src/memory/post-json.ts— 包含需要修复的withRemoteHttpResponse()