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 加上時間戳、模型名和完整報錯文字,就能把那一次呼叫查出來。只說「用不了」的反饋沒法排查。

如果你拿到了狀態碼,這頁不是你要的頁

有數字回來,就說明四級全部成功。連線是通的,是伺服器選擇這樣回答你。

相關報錯