Connection error — 四种完全不同的失败,共用同一个名字
- 一句 Connection error 底下有四种不同的失败,按 DNS → TCP → TLS → HTTP 逐层试,第一个断掉的那级就是答案。
- 拿到状态码就说明网络层没问题,该去看对应状态码的页;连状态码都拿不到才是网络或代理。
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 本身就是答案——ENOTFOUND 和 ECONNREFUSED 不是同一个问题的两种说法,它们是两个不同的问题,只是碰巧被同一行代码打印出来。
如果客户端只打印 Connection error、不带 errno,也不用卡在这里。下面这把梯子就是用手把 errno 找回来。
这把梯子
每一级都依赖它上面的每一级。第一个失败的那一级是唯一值得查的——在它下面的环节根本没有跑过。
| 级数 | 这一级证明了什么 | 失败长什么样 | 意味着什么 | 下一步去哪 |
|---|---|---|---|---|
| 1 · DNS | 名字能对应到地址 | getaddrinfo ENOTFOUND、EAI_AGAIN | 主机名写错,或你的解析服务器答不出来 | 改主机名,或修 DNS——查到这里就停 |
| 2 · TCP | 那个地址在那个端口上接受连接 | ECONNREFUSED、ETIMEDOUT | 端口没开、被拦掉,或中间卡了一个代理 | 防火墙/代理/端口——看第 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 refused(ECONNREFUSED) → 有人回答了,答案是「不」。那个端口上没有东西在监听。Connection timed out(ETIMEDOUT) → 根本没人回答。包被丢掉了:防火墙、网络策略,或一条哪儿都到不了的路由。
做任何事之前,先看报错里的那个地址。如果 ECONNREFUSED 后面写的是 127.0.0.1 或 localhost——就像本页最上面第二段报错那样——那你的请求根本没送到端点。是某个代理环境变量把客户端指向了本机的一个端口,而原本在那儿监听的东西没在跑。光这一个细节,就能解掉相当一部分「它坏了但我什么都没动」的案例,而你把端点查到天亮也查不出这件事。
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.9 或 tcp=4.8 不算失败,但是个警告:某个环节慢到一定程度时,超时设置比 curl 短的客户端就会报 Connection error,而 curl 这边是成功的。
5 · 梯子上没有的那一级 — 客户端走的路,可能和 curl 不一样#
这种情况最让人怀疑自己的眼睛:同一台机器、同一秒,curl 通了,客户端不通。
常见的解释是代理环境变量。curl 会读 HTTP_PROXY、HTTPS_PROXY 和 NO_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 加上时间戳、模型名和完整报错文本,就能把那一次调用查出来。只说「用不了」的反馈没法排查。
如果你拿到了状态码,这页不是你要的页
有数字回来,就说明四级全部成功。连接是通的,是服务器选择这样回答你。
- 401 → 送达了服务器,凭证被拒。请看 401 Unauthorized。
- 403 → 送达了、身份也认出来了,但不让你做。请看 403 Forbidden。
- 429 / 529 → 送达了服务器,而它要你等一下。请看 429 Too Many Requests。
相关报错
- TLS 证书错误 — 为什么它第一次就失败且不会重试
- 401 Unauthorized — 两层认证,一样的报错信息
- 403 Forbidden — 通过认证但没有权限
- 429 Too Many Requests / 529 Overloaded — 服务器回应了,只是要你等一下
- 全部报错速查 — 排查手册总表