Troubleshooting · TLS 證書

unable to get local issuer certificate — 第一次就失敗、而且永遠不重試的那個報錯

30 秒版
  1. 秒失敗、每次都失敗=問題在本地。除證書校驗外幾乎一切失敗都會被自動退避重試,能立刻拋到你臉上的恰恰是不重試的那類。
  2. 匯出你所在網路的 CA 證書,用 NODE_EXTRA_CA_CERTS 指向它,然後重啟客戶端
  3. 不要設 NODE_TLS_REJECT_UNAUTHORIZED=0——它關掉的是整個程序的 TLS 校驗,把一個配置問題換成一個安全問題。

客戶端和端點之間出的岔子,絕大多數都會被自動重試掉——伺服器錯誤、過載、請求超時、斷連、臨時限流,都會指數退避重試最多 10 次,然後才可能讓你看見。TLS 證書驗證失敗是極少數一次都不重試的,它在第一次嘗試就把錯誤丟擲來。這件事把判斷方向整個反過來了:一個瞬間就出現、而且每次都出現的報錯,不是「這家服務不穩」的證據,恰恰是「問題在本地、可復現、歸你修」的證據。

你看到的報錯

以下任意一種
Error: unable to get local issuer certificate

Error: self-signed certificate in certificate chain

{ code: 'SELF_SIGNED_CERT_IN_CHAIN' }
{ code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' }

SSL certificate verification failed
SSL certificate error (...)

這四段說的是同一件事,只是站在不同高度描述。你的執行時拿到伺服器出示的證書,想沿著證書鏈一路走到一個它本來就信任的根,走不通。報錯資訊告訴你的是在哪一步斷的

錯誤碼你會看到的資訊鏈在哪裡斷
UNABLE_TO_GET_ISSUER_CERT_LOCALLYunable to get local issuer certificate出示的證書的簽發者不在你的信任庫裡
SELF_SIGNED_CERT_IN_CHAINself-signed certificate in certificate chain鏈裡有一張自己籤自己的證書——通常是某個代理的私有根
UNABLE_TO_VERIFY_LEAF_SIGNATUREunable to verify the first certificate伺服器只發了葉證書,沒發中間證書
CERT_HAS_EXPIREDcertificate has expired問題在日期,不在信任
老版本 OpenSSL 列印的是 self signed certificate in certificate chain——沒有連字元。同一個故障。兩種拼法都要搜。

這些都不代表伺服器掛了。真掛了的伺服器,根本走不到「出示證書」這一步。

讓這個報錯顯得比實際嚴重的,是重試的不對稱

故障重試預算你實際的體感
伺服器錯誤、過載、在任何響應開始流式返回之前的請求超時指數退避最多 10 次卡一下,然後通常就成功了——你往往根本不知道發生過
斷連,或你的機器休眠導致的斷連同一份預算同上
臨時 429 限流同一份預算同上
響應流卡住、且還沒有任何內容到達額外重發一次,不佔那 10 次預算同上
TLS 證書驗證失敗沒有——第一次嘗試就報出來瞬間、每次、不卡頓
TLS 握手超時照常重試——它是瞬時故障,不是驗證失敗卡一下,然後通常就成功了

把這張表當成一個篩子來讀:有重試預算的故障,必須連續失敗 10 次、而且每次間隔越來越長,才輪得到你看見。沒有預算的,第一次就到你眼前。所以出現得越快的報錯,越系統性地偏向「沒人給它重試」的那一類。這意味著很大一部分被人描述成「這服務時好時壞」的故障,性質其實是反的:一個穩定的、本地的、可復現的配置問題,只是它和終端之間恰好沒有任何緩衝。

落到實用一句話:30 秒才蹦出來的報錯,和瞬間蹦出來的報錯,不是同一類問題,哪怕文案長得像。出錯的快慢本身就是診斷資訊。在動手改任何東西之前先用它。

兩條必須說清楚的限定。證書失敗並不是一直都免於重試的——早期版本會讓它先走完整個重試預算,所以在舊版本上,證書問題看起來也是又慢又飄的。另外,「TLS 報錯」不等於「不重試」:握手超時是瞬時故障,照樣重試。把出錯快慢當訊號用之前,先在你自己正在跑的那個客戶端版本上確認一次實際行為。

到底是哪一層壞了

發生了什麼怎麼認出來誰能修
你的執行時信任庫CA 裝在作業系統裡了,但執行時讀不到系統信任庫npm 安裝 + 較老的 Node;同一個客戶端換原生安裝就正常
TLS 檢查型代理有東西把 TLS 拆開,再用私有根重新簽名出示的簽發者不是公共 CA你(把 CA 加進去)或公司 IT
CA bundle 根本沒進到程序裡NODE_EXTRA_CA_CERTS 設了,但檔案沒被讀進去狀態介面顯示了路徑,除錯日誌裡卻沒有「已追加」那行
缺中間證書伺服器只發葉證書UNABLE_TO_VERIFY_LEAF_SIGNATURE,且別人在別的機器上一樣報錯端點運營方
端點自己的證書過期問題在日期,不在信任簽發者確實是公共 CA,而 notAfter 已經過去端點運營方

多數同類文章上來就是「把證書加上」。這對第二、第三行有用,對第一、第四、第五行完全沒用。下面這一節的順序,是為了讓你在動手改之前先知道自己在哪一行。

按這個順序排查

1 · 先看清楚對方實際出示的是什麼證書#

這是全頁資訊量最大的一條命令。在形成任何猜測之前先跑它。

終端
HOST=api.9coding.com
openssl s_client -connect "$HOST:443" -showcerts </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -subject -dates

issuer 那一行:

  • 是你認得的公共 CA → 沒有人在中間拆你的流量。去看日期;如果 notAfter 已經過去,這是運營方的問題,不是你的。
  • 是你公司的名字、你們安全產品的名字,或者你們內網的某個主機名 → 你的流量在傳輸途中被終止並重新簽名了。這就是全部病因。跳到第 3 步。
  • 命令卡住或什麼都沒返回 → 你碰到的是連通性問題,不是證書問題。請看連線錯誤

2 · 確認執行時讀得到系統信任庫#

基於 Node 的客戶端,既信任自帶的那套 CA,信任作業系統的信任庫——但讀系統信任庫要求執行時版本夠新。版本不夠時,IT 明明在全機範圍內正確裝好的證書,對這個客戶端是不可見的,而機器上其他一切照常工作。瀏覽器好使。curl 好使。只有這個客戶端報錯。

終端
node -p "process.version"
node -p "typeof require('tls').getCACertificates"   # "function" 表示讀得到系統信任庫

如果列印出 undefined,系統信任庫就沒參與,只有自帶的那套加上 NODE_EXTRA_CA_CERTS 生效。要麼升級執行時,要麼繼續走第 3 步——第 3 步在兩種情況下都管用。

3 · 匯出你們機構的 CA,然後讓客戶端指過去#

macOS
security find-certificate -a -p /Library/Keychains/System.keychain > ~/corp-ca.pem
security find-certificate -a -p /System/Library/Keychains/SystemRootCertificates.keychain >> ~/corp-ca.pem
Windows · PowerShell
Get-ChildItem Cert:\LocalMachine\Root, Cert:\LocalMachine\CA |
  ForEach-Object {
    "-----BEGIN CERTIFICATE-----"
    [Convert]::ToBase64String($_.RawData, 'InsertLineBreaks')
    "-----END CERTIFICATE-----"
  } | Set-Content -Encoding ascii $HOME\corp-ca.pem

Linux——先把根證書按正規方式裝進去,再用系統 bundle:

Linux · 終端
sudo cp corp-root.crt /usr/local/share/ca-certificates/corp-root.crt   # Debian/Ubuntu
sudo update-ca-certificates
# RHEL/Fedora:放 /etc/pki/ca-trust/source/anchors/ 然後 `sudo update-ca-trust`

然後把變數指向 bundle,並重啟客戶端——這些變數在啟動時讀一次,正在跑的程序會一直沿用它啟動時的環境:

終端
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt   # Debian/Ubuntu
# macOS/Windows:用你剛寫出來的那個檔案

在怪罪別的東西之前,先驗證這個 bundle 真的能讓證書鏈驗通:

終端
openssl s_client -connect "$HOST:443" -CAfile "$NODE_EXTRA_CA_CERTS" </dev/null 2>&1 \
  | grep -i 'verify return code'
# 要的是:Verify return code: 0 (ok)

4 · 確認 bundle 被載入了,不只是被配置了#

這是兩件事,而且絕大多數「我早就設了」的對話就卡在這裡。狀態介面顯示了路徑,說明的是變數設上了,不說明檔案被讀進去了。路徑讀不到、許可權不對、PEM 換行符不對,都會造出「配了但沒載入」的狀態——從外面看一模一樣。

帶除錯日誌啟動客戶端,找那一行點名檔案的記錄:

終端
claude --debug
# 然後讀 ~/.claude/debug/ 下最新的那個檔案

出現一行確認「已從 NODE_EXTRA_CA_CERTS 追加額外證書」並寫出你的路徑,說明載入成功。出現 failed to readfailed to load 會告訴你原因。一行都沒有,也是失敗——那說明變數根本沒進到程序裡。這在兩種情況下天天發生:變數在一個 shell 裡 export,客戶端卻是從另一個 shell 啟動的;或者客戶端跑在某個守護程序、後臺代理底下,那東西壓根沒見過你的 shell。屬於這種情況,就別寫進 shell 配置檔案,直接寫進客戶端自己的設定檔案。

5 · 把客戶端排除在外——但要分兩路#

終端
# <模型 id>:從 https://api.9coding.com/v1/models 裡複製一個
curl -sS -w '\n%{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"}]}'
終端
node -e 'require("https").get("https://api.9coding.com/", r => console.log(r.statusCode))
  .on("error", e => console.error(e.code, "-", e.message))'

兩條都要跑,因為它們讀的不是同一個信任庫,而兩者的差別就是答案:

  • curl 成功、Node 那條失敗 → 證書在全機範圍內是好的,問題出在執行時的信任庫。回到第 2、3 步。這是最常見的結果,也正是為什麼「可是我瀏覽器能上」什麼都證明不了。
  • 兩條都報證書錯 → 這臺機器上沒有任何東西信任那條鏈。第 1 步已經告訴你為什麼了。
  • 兩條都成功、客戶端還是失敗 → 客戶端讀的不是你改的那份環境。看第 4 步最後一段。

那個你一定會想設、但不該設的變數

每一個討論這個報錯的帖子,最後都會冒出 NODE_TLS_REJECT_UNAUTHORIZED=0。它確實能讓報錯消失。它實際做的事是這樣的。

它關掉的是整個程序的 TLS 證書校驗——不是某一個主機,也不是某一次請求。這個程序往外發的每一條 HTTPS 連線,包括你根本沒想到的那些,從此會接受任何能坐在你和目的地之間的人遞過來的任何證書。而在一臺流量本來就正在被攔截的機器上,這恰好是你原本用來察覺這件事的那個性質。

你手上原本是一個配置問題,第 3 步兩分鐘就能解決。這一下把它換成了一個安全問題:它是無聲的,在有人發現之前會一直在,而且會跟著你複製過去的任何指令碼和映象一起擴散。如果你確實為了五分鐘的實驗設過一次,就在同一個會話裡把它取消掉。

不存在「只是在內網端點上測一下所以沒關係」的版本。信任一個指定的 CA 是同樣的工作量,而且它讓其他所有連線的校驗都保持開著。

什麼時候這不是你的問題

以下情況說明你看到的是運營方的證書,不是你的配置:

  • 第 1 步顯示簽發者確實是公共 CA,而 notAfter 已經過去,
  • 或者第 1 步顯示只有葉證書、沒有中間證書,且 curl 報一樣的錯,
  • 並且換一臺機器、換一個網路也能復現——手機熱點就夠了,
  • 而且它是在你這邊沒有任何改動的情況下開始的。

證書過期是一件有人漏掉的排期事件,所以它通常以小時計恢復,不是以分鐘計。在你花一下午重建信任庫之前,先去看運營方的狀態頁——我們的在狀態頁

提問題時請帶上 request id——9Coding 的報錯響應裡都有一個,形如 (request id: 2026...)。這個 id 加上時間戳、模型名和完整報錯文字,就能把那一次呼叫查出來。只說「用不了」的反饋沒法排查。

證書錯誤和連線錯誤不是同一個問題

證書錯誤意味著連線建立成功了,然後校驗沒過——你已經走到了「被遞了一張證書」這一步。ECONNREFUSEDETIMEDOUTfetch failed 則表示你根本沒走到那裡,配多少 CA 都不會有任何變化。它們還會被重試,這正是它們感覺慢、而這一個感覺瞬間的原因。請看連線錯誤

相關報錯