UrsaKit 封面圖:REST API 的 HTTP 狀態碼選擇指南

能活過 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 CreatedPOST 建立了資源帶上 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 FailedIf-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 需要一個狀態碼時,按這個順序問:

  1. 伺服器有沒有做到被要求的事? 有,就是 2xx——看是建立了、排隊了,還是回傳了內容,決定 200、201、202 或 204。
  2. 如果不是,是呼叫端的問題嗎? 請求格式壞掉是 400;憑證缺失或錯誤是 401;已驗證但沒權限是 403;資源不存在是 404;狀態衝突是 409;語法正確但值不合法是 422;請求太多是 429。
  3. 如果都不是,是我們的問題嗎? bug 是 500;上游不可用是 502;過載或維護中是 503 並帶 Retry-After。

接著檢查契約細節:201 要 Location、405 要 Allow、401 要 WWW-Authenticate、429 與 503 要 Retry-After、204 的 body 必須是空的。

上線前用 HTTP 狀態碼對照 確認一次——那裡的說明是寫給「正在做決定的人」看的,不是寫給規格律師看的。如果錯誤回應是 JSON,也順手用 JSON 驗證工具 跑一遍:連解析都失敗的錯誤回應,比沒有錯誤回應更糟。

常見問題

用戶無權存取的資源,該回 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。

返回文章列表

相關文章

相關工具