
內容摘要
狀態碼是 API 契約的一部分:它告訴客戶端該重試、該重新驗證、該修請求,還是該顯示錯誤。選錯會直接弄壞重試與緩存。
能活過 code review 的 API bug,多半不是邏輯 bug,而是狀態碼 bug。handler 回了 200,body 卻是 { "error": ... },於是流動端顯示成功畫面、CDN 把這個失敗緩存十分鐘、重試函數庫一次都沒觸發。狀態行是基礎設施會讀的那一段,body 才是你的應用程式讀的。
這篇按你實際要做的決策順序,講那些真正需要選的狀態碼。任何一個碼都可以在 UrsaKit 的 HTTP 狀態碼對照 查到,並附上它出自哪份 RFC。
01狀態碼是契約,不是標籤
一個狀態碼有三種讀者,其中只有一種是你的應用程式:
- 緩存依 2xx / 4xx / 5xx 與
Cache-Control之類的標頭,決定要不要存這份回應。 - 代理與閘道把 5xx 當成來源端故障,可能因此觸發斷路器。
- 客戶端函數庫依狀態碼決定要重試、更新權杖,還是直接把錯誤顯示給用戶。
HTTP/1.1 201 Created
Location: /v1/orders/8f14e45
Content-Type: application/json
{ "id": "8f14e45", "status": "pending" }
沒有人是為了好玩才解析狀態行,是因為重試邏輯取決於它。
022xx:客戶端可以假設成功了
| 狀態碼 | 用在 | 注意 |
|---|---|---|
200 OK | 成功的讀取、更新或動作,且要回傳內容 | 失敗時不要用它 |
201 Created | POST 建立了資源 | 帶上 Location 指向新資源 |
202 Accepted | 工作已排入佇列但尚未完成(任務、匯出、webhook) | 回一個可輪詢的狀態網址 |
204 No Content | 成功但沒有內容可回(DELETE、無 body 的 PUT) | body 必須是空的 |
206 Partial Content | 媒體的範圍請求 | 需要 Content-Range |
最值得分清楚的是 200 與 202。如果客戶端必須輪詢,這個請求就還沒結束,202 能在客戶端開始懷疑「寫入是不是丟了」之前就把話說清楚。
033xx:重導與緩存
| 狀態碼 | 意義 | 重導後的方法 |
|---|---|---|
301 Moved Permanently | 永久搬移 | 瀏覽器可能把 POST 改成 GET |
302 Found | 暫時搬移 | 瀏覽器可能把 POST 改成 GET |
307 Temporary Redirect | 暫時搬移 | 方法與 body 都會保留 |
308 Permanent Redirect | 永久搬移 | 方法與 body 都會保留 |
304 Not Modified | 條件式請求命中 | 不回 body,沿用緩存 |
API 端點請優先選 307 或 308:它們是唯二保證方法與 body 能活著跳過這一站的狀態碼。301 與 302 在歷史上太常被客戶端改寫成 GET。
044xx:問題出在客戶端的請求
大部分的設計決策都集中在這一區,因為「請求壞掉」是一整個家族,不是單一狀態碼。
| 狀態碼 | 什麼時候用 | 客戶端通常怎麼修 |
|---|---|---|
400 Bad Request | 請求格式壞掉:JSON 破損、型別錯誤、缺必填欄位 | 修 payload |
401 Unauthorized | 沒有憑證,或權杖過期、無效 | 重新驗證;必須帶 WWW-Authenticate |
403 Forbidden | 已驗證身分,但沒有權限 | 申請權限;原樣重試沒有用 |
404 Not Found | 資源不存在,或不能被揭露 | 停止重試 |
405 Method Not Allowed | 路徑存在,但不支援這個方法 | 換方法;必須帶 Allow |
406 Not Acceptable | 沒有任何表示法符合 Accept | 要求支援的型別 |
409 Conflict | 與目前狀態衝突:重複鍵、版本不符 | 重新讀取後再試一次 |
410 Gone | 資源曾經存在,且已永久移除 | 從書籤與索引中移除 |
412 Precondition Failed | If-Match 或 If-Unmodified-Since 失敗 | 重新取得後再試 |
413 Content Too Large | 上傳超過限制 | 送小一點的內容 |
415 Unsupported Media Type | 不接受這個 Content-Type | 送 JSON,而不是表單 |
422 Unprocessable Content | 語法正確但語意錯誤 | 修正欄位值 |
429 Too Many Requests | 超過速率限制 | 依 Retry-After 等待 |
其中有三個狀態碼帶強制標頭,漏掉就把一個有用的回應變成令人困惑的回應:401 要 WWW-Authenticate、405 要 Allow、429 應該帶 Retry-After。
401 和 403 不能互換。
401 的意思是「我不知道你是誰」,客戶端應該去驗證身分。403 的意思是「我知道你是誰,但你還是不行」——重新驗證沒有幫助。把兩者當成同一件事的客戶端,最後會卡在登入循環裡。
403 與 404 的選擇是安全決策。如果「資源存在」本身敏感——別人的租戶訂單編號、還沒發布的文件——就回 404,讓回應無法被拿來列舉。如果客戶端可以知道資源存在、只是不能動它,那 403 更誠實。
055xx:伺服器或其依賴出了問題
| 狀態碼 | 意義 | 可重試 |
|---|---|---|
500 Internal Server Error | 未處理的例外 | 通常不行,同樣的請求會再失敗 |
501 Not Implemented | 伺服器完全不支援這個方法 | 不行 |
502 Bad Gateway | 上游回傳了無法使用的內容 | 有時候可以 |
503 Service Unavailable | 過載或維護中 | 可以,依 Retry-After |
504 Gateway Timeout | 上游沒有在時限內回應 | 有時候可以,但要考慮逾時預算 |
維護與過載期間,請回 503 並帶上 Retry-After。沒有這個標頭,守規矩的客戶端只能退回指數退避;而用 HTTP 402 讓代理付費 這類做法之所以出現,正是因為明確的機器可讀訊號比猜測可靠得多。
不要把真正的失敗包在 200 裡。
帶錯誤物件的 200 會弄壞緩存、讓監控看不到失敗,並關掉每個 HTTP 客戶端本來就有的重試邏輯。回應裡有錯誤,狀態碼就必須說出來。
06值得修掉的常見錯誤
| 錯誤 | 傷害 | 修正 |
|---|---|---|
| 所有回應都回 200 | 監控、緩存、重試全都看到成功 | 用符合結果的狀態碼 |
| 權限錯誤回 401 | 客戶端清掉 session,再跳回登入 | 改用 403 |
| 所有驗證失敗都回 400 | 客戶端分不出「修語法」還是「修值」 | 語法用 400,語意用 422 |
| 客戶端錯誤回 500 | 為了一個呼叫端的 bug 叫值班 | 驗證失敗對應到 4xx |
| 方法錯誤回 404 | 客戶端無法得知允許哪個方法 | 回 405 並帶 Allow |
429 不帶 Retry-After | 客戶端只能亂猜,然後一起湧上來 | 送出標頭 |
| 204 卻帶 body | 代理與客戶端對訊息框架的看法不一致 | 改用 200,或把 body 拿掉 |
07用三個問題決定
handler 需要一個狀態碼時,按這個順序問:
- 伺服器有沒有做到被要求的事? 有,就是 2xx——看是建立了、排隊了,還是回傳了內容,決定 200、201、202 或 204。
- 如果不是,是呼叫端的問題嗎? 請求格式壞掉是 400;憑證缺失或錯誤是 401;已驗證但沒權限是 403;資源不存在是 404;狀態衝突是 409;語法正確但值不合法是 422;請求太多是 429。
- 如果都不是,是我們的問題嗎? bug 是 500;上游不可用是 502;過載或維護中是 503 並帶
Retry-After。
接著檢查契約細節:201 要 Location、405 要 Allow、401 要 WWW-Authenticate、429 與 503 要 Retry-After、204 的 body 必須是空的。
上線前用 HTTP 狀態碼對照 確認一次——那裡的說明是寫給「正在做決定的人」看的,不是寫給規格律師看的。如果錯誤回應是 JSON,也順手用 JSON 驗證工具 跑一遍:連解析都失敗的錯誤回應,比沒有錯誤回應更糟。
1xx 到 5xx 每個狀態碼的白話解釋,以及它出自哪一份 RFC。
HTTP 狀態碼對照常見問題
用戶無權存取的資源,該回 404 還是 403?
若「資源存在」本身就是機密(例如其他租戶的訂單編號),回 404,避免被拿來列舉。若客戶端本來就知道資源存在、只是沒權限,回 403 更誠實。
回 200 但 body 裡放錯誤資訊可以嗎?
不行。200 會告訴所有中介層、緩存與重試函數庫「這次成功了」,客戶端得先解析 body 才知道失敗,監控也永遠看不到。
什麼時候該用 422 而不是 400?
請求本身壞掉(JSON 格式錯誤、缺必填欄位、型別不對)用 400。語法正確但語意不對(例如結束日期早於開始日期)用 422。
401 與 407 差在哪?
401 表示請求缺少有效的目標資源憑證,且必須帶 WWW-Authenticate 標頭。407 是代理伺服器版本,用的是 Proxy-Authenticate。
哪些狀態碼應該重試?
408、425、429、503 明確可重試,最好依照 Retry-After。500 與 502 通常代表伺服器端 bug,盲目重試只會重複同樣的失敗。
204 可以帶 body 嗎?
不行。204 代表沒有內容,body 必須是空的。若需要回傳被刪除的物件,改用 200 帶 body。
相關文章
- JSON 格式化、驗證與壓縮
讓錯誤回應的 body 和成功回應一樣是機器可讀的。
- 用 Diff 比對兩段文字
比對兩份失敗的 API 回應,找出變動的欄位。
相關工具
- HTTP 狀態碼對照
查任何狀態碼,以及客戶端會怎麼反應。
- JSON 格式化與驗證
驗證跟著狀態碼一起送出的內容。
- Base64 編解碼工具
檢查授權標頭與權杖。
- 正則表達式測試器
從日誌裡依狀態碼比對與改寫。