HTTP 狀態碼:設計 API 時,這個該回哪一個?

不背整張表,只記三個問題 —— 寫給每次都在猶豫「回 400 還是 422」的人

先講一句話

狀態碼是回應的第一行摘要,寫給不會讀 body 的人看的。

誰不讀 body?瀏覽器、CDN/快取、SDK 的自動重試、你的監控告警、負載平衡器。它們只看那三個數字就決定要不要重送、要不要快取、要不要把你叫醒。

為什麼你需要它

想像一間餐廳,不管你點的菜有沒有做出來,服務生都笑著說「好的沒問題」,然後在收據角落用小字寫「其實廚房失火了」。

這就是「全部回 200,錯誤寫在 body 裡」的 API。後果不是理論上的:

狀態碼給機器看,body 給人看。兩個都要,但別把機器要的那份藏在 body 裡。

核心對比:全場最重要的一條線

五個家族只是開胃菜(1xx 還在處理、2xx 成功、3xx 換個地方拿、4xx 請求有問題、5xx 伺服器有問題)。真正每天在用的是這條線:

4xx = 「你要改」
一模一樣的請求再送一百次還是會失敗。是呼叫方要改參數、補權限、換做法。重試沒有意義。
5xx = 「我要修」
請求本身沒問題,是我這邊爆了。同樣的請求等一下可能就成功。該被叫醒的是我。

這條線不只是語意潔癖 —— 它決定了誰的告警會響、重試機制會不會自動重送、SLA 的錯誤率算在誰頭上。把自己的 bug 回成 400,等於把自己的鍋丟給呼叫方;反過來把參數錯回成 500,你會被自己的告警半夜叫醒去看一個根本沒壞的服務。

那 1xx 和 3xx 呢?(點開看) 1xx 是「收到了,還沒完」,日常自己寫 API 幾乎用不到(101 是升級成 WebSocket 時用的)。3xx 是「東西不在這,去那邊拿」:301/308 永久搬家、302/307 暫時的、304 Not Modified 是快取專用(配 ETagIf-None-Match,代表「你手上那份還新鮮,我不用再傳一次」)。分類定義見 RFC 9110 §15。
把抽象變成動作 ⚙️

設計時不要從「有哪些碼」開始想,那是查表。從三個問題開始問,答完就只剩兩三個候選:

手上這個回應該回什麼?照著 ①②③ 問一輪,答案自己會浮出來

手把手:三步挑一個碼
  1. 成功了嗎? → 挑 2xx
    有東西要回 → 200 OK
    建立了一筆新資源 → 201 Created,順手在 Location 標頭放新資源的網址。
    收下了但還沒做完(丟進佇列、非同步處理)→ 202 Accepted,回一個可以查進度的位置。
    做完了但沒東西好回(刪除、純更新)→ 204 No Contentbody 必須是空的
  2. 失敗了,是誰的錯? → 4xx 還是 5xx
    判準就一句:同樣的請求再送一次,有機會成功嗎?
    沒機會(少參數、沒權限、東西不存在)→ 4xx。
    有機會(DB 剛好斷線、上游超時、我沒接到的例外)→ 5xx。
    拿不定主意時想:這個錯該進誰的待辦清單?
  3. 選定家族後,挑最貼切的那個
    別只會 400500,但也不必用滿。挑一組你團隊講得清楚的常用碼(下面那張表就夠用),寫進規範,全站一致 —— 一致比精準更值錢。
    挑不到貼切的?回家族的通用碼(400500)就好,然後把細節寫在 body。
📋 常用碼速查(會用這些就夠了)
白話什麼時候回
200 OK好了,東西在這查詢成功、更新成功且要回內容
201 Created幫你建好了新增資源成功;附上 Location
202 Accepted收下了,還在做非同步任務、排入佇列
204 No Content做完了,沒東西回刪除成功、無回傳的更新
400 Bad Request你這包我讀不懂JSON 壞掉、少必填、型別錯、格式不合
401 Unauthorized你是誰?沒帶憑證或憑證過期/無效。要附 WWW-Authenticate
403 Forbidden知道你是誰,但不行身分沒問題,就是沒這個權限
404 Not Found沒這東西路徑不存在、ID 查無資料
405 Method Not Allowed路徑對,動詞錯對只讀端點送 DELETE。要附 Allow 列出可用方法
409 Conflict跟目前狀態打架重複註冊、樂觀鎖版本衝突、狀態機不允許的轉換
415 Unsupported Media Type這格式我不吃送了 XML 但我只收 JSON
422 Unprocessable Content看得懂,但不合理格式完全正確,是商業規則不通過(生日在未來、金額超上限)
429 Too Many Requests太快了,慢點觸發限流。Retry-After 告訴對方等多久
500 Internal Server Error我壞了,還不知道為什麼沒接住的例外。別把細節或堆疊吐給外面
502 Bad Gateway我後面那台回了垃圾上游回了無效回應/連不上
503 Service Unavailable暫時不能服務維護中、過載、熔斷開啟。附 Retry-After
504 Gateway Timeout我後面那台太慢等上游等到超時

標色的那幾條是八成情況會用到的;其餘先知道存在,用到再查。

那 body 要長什麼樣?(點開看) 狀態碼只有三位數,講不出「哪個欄位錯了」。錯誤細節放 body,格式別每支自己發明 —— 有現成標準 RFC 9457 Problem Detailsapplication/problem+json,欄位如 typetitledetailinstance),照它做,前端和第三方接起來都省事。
🥊 最常吵的四組
400 還是 422?
400=我連讀都讀不懂(JSON 壞了、少必填、型別錯)。
422=我讀懂了,但內容不合規則(生日填未來、轉帳金額超過餘額上限)。
分不清就全站統一用 400,比一半一半好。
401 還是 403?
401還不知道你是誰(沒登入、token 過期)→ 前端該做的是跳登入。
403知道你是誰,就是不給(一般用戶想開管理頁)→ 跳登入沒用,重登一百次還是不行。
403 還是 404?
「這筆資料存在,但不是你的」——照實回 403 等於告訴對方這個 ID 真的存在,可以被拿來掃描猜測。
敏感資源常刻意回 404(裝作不存在)。這是安全取捨,不是語意錯誤,但要全站一致,別半套。
502 / 503 / 504 差在哪?
都是 5xx,差在誰壞了
502=我後面那台回了垃圾或連不上。
504=我後面那台還活著,只是太慢,我等到超時。
503我自己現在不服務(維護/過載/熔斷)。查問題時這三個直接指向不同的地方。
順便:301 / 302 / 307 / 308 的坑(點開看) 歷史包袱:早期瀏覽器遇到 301302 會把 POST 悄悄改成 GET 再送一次(body 就沒了)。307308 是後來補的,明確規定方法和 body 都要原封不動。所以:需要保留方法 → 用 307(暫時)/308(永久)。另外 301308 會被瀏覽器長期記住,改錯了很難收回,搬家沒搬乾淨前先用暫時的那組。
✍️ 換你試幾題

心裡先答,再往下想理由(沒有標準答案警察,但每題都有明顯較好的選擇):

  1. 刪除一筆待辦事項,成功了,沒東西要回給前端。
  2. 使用者上傳頭像,你丟進佇列慢慢轉檔,馬上要回應。
  3. 註冊時 email 已經有人用了。
  4. 手機 App 帶著三天前過期的 token 來打 API。
  5. 你呼叫金流商的 API,等了 30 秒沒回應,你的 API 該回什麼給 App?
看看我的答案 1. 204(做完了、沒 body)。2. 202 + 一個查進度的位置(別回 200 假裝做完了)。3. 409(跟現況衝突)——若你把「email 重複」當成欄位驗證失敗,回 400/422 也說得通,重點是全站一致。4. 401(是「你是誰」的問題,不是權限問題)。5. 504(上游超時),不是 500——分清楚,你才會去查金流商而不是查自己。
✔ 過關標準

面對任何一個回應,你能三秒內說出該回哪個碼、而且講得出理由;並且在 4xx/5xx 之間不會擺錯邊。

你不需要背得出 418 是什麼(真的有這個碼,是愚人節玩笑的茶壺)。你需要的是:挑一組常用碼、寫進團隊規範、全站用一樣的規則。

帶走這一句

先問「是誰的錯」,再問「等一下會不會好」——
剩下的就只是查表。

📚 想深讀(權威來源)

*「三個問題」的挑碼流程是本文為了好記自行整理的教學框架,非引自單一來源;每個碼的語意定義則依 RFC 9110 / IANA。

chance · 學習文章 · 2026-09-01 13:26