Skip to main content

先說結論:錯誤原文只在響應體裡出現一次

API易 的錯誤資訊只通過介面響應體返回給你。後臺日誌是計費賬本——它記錄的是成功產生扣費的呼叫,報錯請求既不扣費、也不會出現在日誌裡。所以「後臺日誌裡查不到」並不等於「沒發生」,而是意味著那次錯誤的唯一記錄就在你的客戶端。你沒把它列印下來、落到盤上,它就永久消失了——連我們也找不回來。
一句話結論:把介面返回的原始響應體原樣打出來,不要只留你程式包裝過的那一句話。400 Bad Request 這種字串對定位問題的貢獻接近於零,真正的答案在它下面被丟掉的那個 JSON 裡。

一個真實案例:400 Bad Request 說明不了任何問題

一位客戶上報的原文只有一行:
這行字串是客戶端框架包裝後的產物。它保留了模型名、HTTP 方法、URL 和狀態碼,唯獨把最關鍵的響應體丟掉了。支援側能給出的最好回答只能是:
400 一般是內容安全和引數問題,大機率是內容安全。
這是猜測,不是結論。因為同一次呼叫,介面真正返回的響應體可能是下面三種中的任意一種,而它們對應三種完全不同的處理動作:
同一個 400,三種完全不同的處理動作。 丟掉響應體,就是把「三選一」變成「靠猜」——猜錯的代價是一輪無效的來回溝通,外加一次本來可以避免的重試。更要緊的是:這三種情況裡有兩種根本不該重試。分不清是哪一種,就只能盲目重試,白白消耗時間和額度。

後臺日誌能查到什麼,查不到什麼

這是最需要先建立的認知:後臺日誌是計費賬本,不是錯誤日誌。
反過來用,這是排查斷連類問題最有力的判據:如果日誌裡這條計費記錄,說明請求確實到達了上游併產生了消耗;如果沒有,那問題多半發生在到達上游之前(網路、鑑權、引數校驗)。完整口徑見怎麼看懂日誌裡的計費金額

必須留存的 7 個欄位

排查一次報錯需要的資訊就這些。缺任何一項都會讓排查退化成猜測:
響應體不要截斷。 常規業務日誌裡截斷到 200 字元是合理的,但排障場景下錯誤詳情經常出現在末尾。至少保留前 2000 字元;圖片類介面若擔心 base64 刷屏,只在 status_code >= 400 時全量列印即可——錯誤響應體本來就不長。

正確的錯誤捕獲寫法

核心原則只有一條:分兩層捕獲,並且在任何一層都不要丟棄原始資訊。
  • 傳輸層異常:連線被重置、TLS 握手失敗、超時、DNS 失敗。此時根本沒有 HTTP 響應,能留的只有異常原文。
  • HTTP 層錯誤:服務端返回了 4xx / 5xx。此時一定有響應體,必須讀出來。

Python / requests

不要在讀響應體之前就 raise_for_status() 它丟擲的 HTTPError 只帶一句 400 Client Error: Bad Request for url: ...,正文原封不動地留在 resp.text 裡沒人去讀——這正是本頁開頭那個案例的成因之一。要用它,也請先把 resp.text 取出來。

Python / OpenAI SDK

官方 SDK 已經把三樣東西都掛在異常物件上了,只是很多人只 print 了一句自己的中文提示:
即便只寫一行,也請寫 print(f"API 錯誤:{e}") 而不是 print("呼叫失敗")——SDK 異常的 str(e)已經包含服務端返回的 message。真正會丟資訊的是把異常物件整個扔掉的那種寫法。

Node.js

用 SDK 時:
直接用 fetch 時,這一步是最容易出事的地方
本頁開頭那個 400 Bad Request from POST https://api.apiyi.com/v1/images/edits,字面上就等於 ${resp.status} ${resp.statusText} from ${resp.method} ${resp.url} ——響應體從頭到尾沒有被讀取過fetch 在 HTTP 層面出錯時不會 rejectresp.okfalse 而已;如果這時直接拋 resp.statusText,body 就隨著響應物件一起被丟棄了。在拋錯之前先 await resp.text(),這一行的差別就是能不能定位問題。

cURL 復現

請客戶復現時,給這條命令最省事——它把狀態碼、響應頭、響應體、耗時一次性全帶出來:
  • -i 列印響應頭,x-request-id 就在裡面;
  • -sS 關掉進度條但保留錯誤輸出;
  • -w 在末尾附加狀態碼與總耗時,方便和超時配置對照。

封裝工具與自研中臺怎麼辦

一個正面例子

下面這條報錯來自某位客戶的 ComfyUI 節點:
它比 400 Bad Request 難看得多,但資訊是完整的,幾秒鐘就能定死方向: 結論直接就出來了:這是傳輸層問題,與內容安全、與引數統統無關,也不會產生計費(請求根本沒完成)。排查路徑見圖片 API 連線中斷排查
對比一下:一個是包裝得很乾淨但什麼也說明不了的 400 Bad Request,一個是又長又醜但直接指向根因的 errno 10054排障場景下,原始、難看、完整的錯誤遠勝於友好、簡潔、被改寫過的錯誤。

常見工具去哪裡找原文

自研中臺的三條原則

1

透傳,不要改寫

中間層可以追加上下文(哪個業務、哪個租戶、第幾次重試),但不能替換上游返回的 error.message。一旦改寫,原文就沒有第二個地方可以找回。
2

面向使用者的提示和麵向開發的原文分開存

參考 Gemini 圖片錯誤處理 裡的三分結構:userMessage(給終端使用者看的友好文案)、devMessage(給開發看的判定結論)、rawResponse(原始響應體,一字不改)。前兩個可以隨便潤色,第三個必須原樣入庫。
3

永遠不要出現「未知錯誤」

走到兜底分支時,把 statusx-request-id 和響應體前 2000 字元一併記下來。一個帶著原文的「未分類錯誤」是可排查的;一句乾淨的「未知錯誤」不是。

反面清單:這些寫法會讓問題無法排查

  • except Exception as e: print("呼叫失敗") —— 異常物件整個被丟掉,連是哪一層的問題都不知道;
  • 只記 HTTP 狀態碼,不記響應體 —— 就是本頁開頭那個案例;
  • raise_for_status() 之前不讀 resp.text —— 正文還在記憶體裡,就是沒人取;
  • fetchif (!resp.ok) throw new Error(resp.statusText) —— body 隨響應物件一起被扔了;
  • 客戶端重試成功後只留一條漂亮的 200 —— 把每次嘗試單獨記一條,否則你永遠看不到底層斷了多少次,還容易把自己的重試誤讀成渠道行為;
  • 日誌只打屏不落盤、或按天覆蓋 —— 等客戶反饋到你這裡時,原始記錄往往已經滾沒了;
  • 報障時只發一張手機拍的螢幕照片 —— 請直接複製文本,截圖裡的錯誤經常正好被裁掉半行。

什麼時候找客服

先自己走完上面的留存與判讀,如果滿足下面任一條,就帶著材料找客服核查:
  • 拿到了完整響應體,但 error.message 指向上游方向(upstream_error、上游 5xx 原文、渠道明確報錯);
  • 同一份請求引數換個模型或換個時間段就正常,只有某個特定模型穩定失敗;
  • 報錯是 500 + write_response_body_failed 這類下行鏈路問題,且穩定復現(這類不計費,排查方法見連線中斷排查);
  • 你懷疑計費與實際呼叫對不上——這時 request_id 是唯一能精確對賬的錨點。

報障資訊模板(可直接複製)

企業微信客服

企業微信客服二維碼掃碼新增,或點選本卡片聯絡企業微信客服。也可通過 Telegram @apiyi001 或郵箱 [email protected] 聯絡我們。
把上面模板裡的內容以文本形式發過來,比任何描述都高效。有 request_id 時我們能直接定位到那一次呼叫的完整鏈路,不必再問「大概幾點呼叫的什麼模型」。request_id 的查詢方法見日誌查詢 API

相關文件

API 手冊

常見錯誤碼對照表、認證方式與速率限制

連線中斷排查

ECONNRESETerrno 10054、SSL EOF 這類傳輸層報錯的完整排查路徑

日誌查詢 API

用介面批次拉呼叫日誌,request_id 怎麼查、怎麼對賬

看懂日誌計費金額

為什麼失敗呼叫不進日誌,以及「有無計費記錄」這條判據怎麼用

超時怎麼配置

各類模型的 timeout 分檔、已調大仍超時的逐層排查

圖片 API 必讀

同步呼叫、base64 字首差異、400 invalid_image_file 的圖片預處理