Troubleshooting · 连接

Connection error — 四种完全不同的失败,共用同一个名字

30 秒版
  1. 一句 Connection error 底下有四种不同的失败,按 DNS → TCP → TLS → HTTP 逐层试,第一个断掉的那级就是答案。
  2. 拿到状态码就说明网络层没问题,该去看对应状态码的页;连状态码都拿不到才是网络或代理。
  3. curl 通而客户端不通,多半是代理环境变量HTTP_PROXY/NO_PROXY)两者读法不一致。

Connection error 不是诊断结果,它是客户端在 HTTP 之下某个环节失败之后,自己写出来的一句总结,而四种完全不同的失败会产生同一句话。用一把四级的 curl 梯子逐层往上爬——DNS、TCP、TLS、HTTP——第一个断掉的那一级就会告诉你问题是什么、该找谁。没爬梯子先猜,是把一个下午花在错误层级上的标准流程。

你看到的报错

以下任意一种
API Error: Connection error.

TypeError: fetch failed
    [cause]: Error: connect ECONNREFUSED 127.0.0.1:7890

Error: connect ETIMEDOUT 203.0.113.10:443

Error: getaddrinfo ENOTFOUND gateway.example.com

第一种什么都没说。另外三种点出了 errno,而 errno 本身就是答案——ENOTFOUNDECONNREFUSED 不是同一个问题的两种说法,它们是两个不同的问题,只是碰巧被同一行代码打印出来。

如果客户端只打印 Connection error、不带 errno,也不用卡在这里。下面这把梯子就是用手把 errno 找回来。

这把梯子

每一级都依赖它上面的每一级。第一个失败的那一级是唯一值得查的——在它下面的环节根本没有跑过。

级数这一级证明了什么失败长什么样意味着什么下一步去哪
1 · DNS名字能对应到地址getaddrinfo ENOTFOUNDEAI_AGAIN主机名写错,或你的解析服务器答不出来改主机名,或修 DNS——查到这里就停
2 · TCP那个地址在那个端口上接受连接ECONNREFUSEDETIMEDOUT端口没开、被拦掉,或中间卡了一个代理防火墙/代理/端口——看第 2 步和第 5 步
3 · TLS握手能完成握手失败、证书错误证书链,或中间有设备在拦检TLS 证书错误
4 · HTTP服务器有回应任何一个状态码网络这一层没问题那个状态码自己的页面

最后一行是最多人漏掉的。只要拿到状态码——任何一个,包括 401、403、429、500——就说明四级全部通过了。手上有数字,就不是连接问题,这页不是你要的页。

按这个顺序排查

1 · 第 1 级 — 名字解析得出来吗?#

终端
HOST=$(echo "https://api.9coding.com" | awk -F/ '{print $3}' | cut -d: -f1)
echo "$HOST"
getent hosts "$HOST" || nslookup "$HOST"
  • 回得出地址 → 第 1 级通过。把地址记下来,第 2 步要用。
  • ENOTFOUND / NXDOMAIN → 要么主机名写错了,要么你的解析服务器答不出这个名字。

要分开这两种,把同一个问题丢给另一个解析服务器:

终端
nslookup "$HOST" <another-resolver-ip>
  • 换一个解析服务器就查得到,用默认的查不到 → 问题在你的 DNS,不在端点。常见原因是 VPN、容器里的解析配置,或一条过期的 /etc/hosts
终端
grep -i "$HOST" /etc/hosts
  • 换哪个解析服务器都查不到 → 一个字一个字核对主机名。到处都解析不出来的名字,几乎都是打错,或者把只在内网有效的名字拿到外面用。

2 · 第 2 级 — 端口到底开没开?#

--resolve 自己把地址钉死,等于把 DNS 完全排除在这个实验之外,这样这里失败就只可能是 TCP。

终端
curl -sS -v --connect-timeout 5 --resolve "$HOST:443:203.0.113.10" \
  -o /dev/null "https://$HOST/" 2>&1 | grep -E 'Trying|Connected|refused|timed out'

203.0.113.10 换成第 1 步打印出来的地址。

  • Connected to ... → 第 2 级通过,去第 3 步。
  • Connection refusedECONNREFUSED → 有人回答了,答案是「不」。那个端口上没有东西在监听。
  • Connection timed outETIMEDOUT → 根本没人回答。包被丢掉了:防火墙、网络策略,或一条哪儿都到不了的路由。

做任何事之前,先看报错里的那个地址。如果 ECONNREFUSED 后面写的是 127.0.0.1localhost——就像本页最上面第二段报错那样——那你的请求根本没送到端点。是某个代理环境变量把客户端指向了本机的一个端口,而原本在那儿监听的东西没在跑。光这一个细节,就能解掉相当一部分「它坏了但我什么都没动」的案例,而你把端点查到天亮也查不出这件事。

Refused 和 timed out 的意思是相反的,这点值得说白:refused 是快速而明确的拒绝,timed out 是沉默。关着的端口会立刻回答你,防火墙什么都不说。

3 · 第 3 级 — TLS 握手完成得了吗?#

终端
openssl s_client -connect "$HOST:443" -servername "$HOST" </dev/null 2>&1 | head -25
  • Verify return code: 0 (ok) → 第 3 级通过,去第 4 步。
  • unable to get local issuer certificate / self-signed certificate in certificate chain → 第 3 级失败在「信任」,不是失败在「连不连得上」。连接建立成功了、然后证书校验没过,这和「根本没走到那一步」是两个不同的问题。请看 TLS 证书错误,别再继续查网络。
  • 证书签发给一个你不认识的名字 → 你和端点之间有东西把 TLS 拦下来重新签了。那是中间拦检设备,也是同一页

有一个特性值得记住,因为它能让你直接跳过这一级:TLS 失败是必然的。同一个客户端、同一份信任库、同一张证书,第一次会失败,之后每一次都会失败。所以只要你的失败是时好时坏的,问题就不在第 3 级。

4 · 第 4 级 — 服务器有回应吗,以及到底断在哪一级?#

只要会读,这一条命令抵得上整把梯子。curl 会报出每一级完成的时间点,没完成的那一级会报 0.000000

终端
# <模型 id>:从 https://api.9coding.com/v1/models 里复制一个
curl -sS -o /dev/null \
  -w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} status=%{http_code}\n' \
  https://api.9coding.com/v1/messages \
  -H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
  -H "content-type: application/json" \
  -H "anthropic-version: 2023-06-01" \
  -d '{"model":"<模型 id>","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
你拿到的结果断在哪一级
dns=0.000000第 1 级——名字从来没解析出来
dns > 0、tcp=0.000000第 2 级——从来没连上
tcp > 0、tls=0.000000第 3 级——连上了,握手失败
tls > 0、status=000连上也谈完了,请求中途死掉——通常是慢响应被超时砍断
status= 任何真实数字四级全部通过。这不是连接问题

把输出留着。dns=1.9tcp=4.8 不算失败,但是个警告:某个环节慢到一定程度时,超时设置比 curl 短的客户端就会报 Connection error,而 curl 这边是成功的。

5 · 梯子上没有的那一级 — 客户端走的路,可能和 curl 不一样#

这种情况最让人怀疑自己的眼睛:同一台机器、同一秒,curl 通了,客户端不通。

常见的解释是代理环境变量。curl 会读 HTTP_PROXYHTTPS_PROXYNO_PROXY。别的客户端可能会读,可能只读小写那组,可能要求你另外配置,也可能完全不理——于是 curl 和客户端会走在两条不同的网络路径上。

终端
env | grep -i -E 'proxy'

然后把第 4 级跑两次——一次照原样,一次把代理变量拔掉——再比对:

终端
# <模型 id>:从 https://api.9coding.com/v1/models 里复制一个
env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy -u ALL_PROXY -u all_proxy \
  curl -sS -o /dev/null -w 'status=%{http_code}\n' https://api.9coding.com/v1/messages \
  -H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
  -H "content-type: application/json" \
  -H "anthropic-version: 2023-06-01" \
  -d '{"model":"<模型 id>","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
带代理变量拔掉代理变量意味着什么
这次失败跟代理无关
不通这个网络必须走代理——不读那些变量的客户端根本连不到端点
不通代理正在弄坏这个请求——把这个主机排除掉
不通不通不是代理的问题,回到第 1 级

还有两件事值得知道:

  • 三个变量里最常设错的是 NO_PROXY。前导点、端口、通配符的匹配规则在各家实现之间并不一致,所以一个对某个工具有效的写法,可能被另一个工具默默忽略。排除规则要实测,不要假设——而且要在你自己正在跑的那个客户端版本上实测。
  • 如果代理变量指向 127.0.0.1,那这台机器上所有读它的程序,都依赖某个本机进程一直活着。那个进程一停,所有读这个变量的客户端会同时报 ECONNREFUSED,而所有不读它的客户端照样能用。这种不对称,看起来非常像「只有某一个 App 坏了」。
改完任何一项都要重启客户端。长时间运行的进程会沿用它启动时的环境。

什么时候这不是你的问题

以下几点同时成立时,你面对的是网络路径或端点,不是你的配置:

  • 第 1 到第 3 级在你这台机器上都通过,请求还是失败,
  • 失败是 ETIMEDOUT 而不是 ECONNREFUSED(是传输途中的沉默,不是关着的端口),
  • 同一个网络上的其他客户端也是一样的错,
  • 而且你什么都没改,过一阵子会自己好。

两个能省下一小时的捷径:

  • 时好时坏的失败,永远不是证书问题。证书要么每次都失败,要么从来不失败。
  • 你手上最快的测试是换一个网络。用手机热点大约三十秒就能分开「我的网络」和「那个端点」,读多少本机日志都没这个快。

确认完再回头动自己的配置之前,先去看运营方的状态页与变更日志——我们的在状态页

提问题时请带上 request id——9Coding 的报错响应里都有一个,形如 (request id: 2026...)。这个 id 加上时间戳、模型名和完整报错文本,就能把那一次调用查出来。只说「用不了」的反馈没法排查。

如果你拿到了状态码,这页不是你要的页

有数字回来,就说明四级全部成功。连接是通的,是服务器选择这样回答你。

相关报错