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 — 伺服器回應了,只是要你等一下
- 全部報錯速查 — 排查手冊總表