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 是快取專用(配 ETag/If-None-Match,代表「你手上那份還新鮮,我不用再傳一次」)。分類定義見 RFC 9110 §15。
把抽象變成動作 ⚙️

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

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

手把手:三步挑一個碼
  1. 成功了嗎? → 挑 2xx
    有東西要回 → 200 OK。
    建立了一筆新資源 → 201 Created,順手在 Location 標頭放新資源的網址。
    收下了但還沒做完(丟進佇列、非同步處理)→ 202 Accepted,回一個可以查進度的位置。
    做完了但沒東西好回(刪除、純更新)→ 204 No Content,body 必須是空的。
  2. 失敗了,是誰的錯? → 4xx 還是 5xx
    判準就一句:同樣的請求再送一次,有機會成功嗎?
    沒機會(少參數、沒權限、東西不存在)→ 4xx。
    有機會(DB 剛好斷線、上游超時、我沒接到的例外)→ 5xx。
    拿不定主意時想:這個錯該進誰的待辦清單?
  3. 選定家族後,挑最貼切的那個
    別只會 400 和 500,但也不必用滿。挑一組你團隊講得清楚的常用碼(下面那張表就夠用),寫進規範,全站一致 —— 一致比精準更值錢。
    挑不到貼切的?回家族的通用碼(400/500)就好,然後把細節寫在 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 Details(application/problem+json,欄位如 type/title/detail/instance),照它做,前端和第三方接起來都省事。
🥊 最常吵的四組
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 的坑(點開看) 歷史包袱:早期瀏覽器遇到 301/302 會把 POST 悄悄改成 GET 再送一次(body 就沒了)。307/308 是後來補的,明確規定方法和 body 都要原封不動。所以:需要保留方法 → 用 307(暫時)/308(永久)。另外 301/308 會被瀏覽器長期記住,改錯了很難收回,搬家沒搬乾淨前先用暫時的那組。
✍️ 換你試幾題

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

  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