Skip to content
BEE
Backend Engineering Essentials

[BEE-4006] API 錯誤處理與問題詳情 ​

INFO

HTTP 狀態碼作為主要錯誤訊號、RFC 9457 問題詳情格式,以及一致的機器可讀錯誤回應。

Deep Dive

For comprehensive API error design patterns and error taxonomy, see ADE (API Design Essentials).

背景 ​

錯誤處理是 API 設計中最常被忽視的面向。許多 API 對所有回應都回傳 200 OK 並在 body 中嵌入成功旗標、在正式環境洩漏內部堆疊追蹤,或在不同端點之間產生完全不一致的錯誤結構。這些模式會傷害 API 的使用者:錯誤處理程式碼變得脆弱、除錯耗時,客戶端開發者也無法建立可靠的重試或告警邏輯。

RFC 9457(HTTP API 的問題詳情),由 IETF 於 2023 年發佈,作為 RFC 7807 的後繼標準,定義了一種標準 JSON 格式來表達 HTTP 錯誤詳情。Stripe、Google 以及大多數設計完善的公開 API,都獨立收斂到相同的原則:正確使用 HTTP 狀態碼、回傳結構化的機器可讀錯誤、包含足夠的上下文讓對方採取行動,且絕不暴露實作細節。

核心規則很簡單:HTTP 狀態碼告訴客戶端失敗的類別;回應 body 告訴它發生了什麼以及下一步該怎麼做。

原則 ​

HTTP 狀態碼作為主要錯誤訊號 ​

RFC 9110 定義了 HTTP 狀態碼的語義。狀態碼不是可選的元資料,而是溝通請求結果的標準通道。堆疊中每個 HTTP 感知元件(負載平衡器、代理、監控工具、CDN、客戶端函式庫)都會檢查狀態碼。繞過它們會破壞整個生態系統。

範圍類別客戶端應對
2xx成功正常繼續
4xx客戶端錯誤修正請求;不要盲目重試
5xx伺服器錯誤伺服器失敗;通常可搭配退避策略重試

錯誤情境必須使用的狀態碼:

代碼名稱使用時機
400Bad Request格式錯誤、缺少必要欄位、無效的參數類型
401Unauthorized缺少或無效的認證憑證
403Forbidden已認證但缺乏授權
404Not Found資源不存在
409Conflict請求與當前資源狀態衝突(重複建立、版本不符)
410Gone資源曾存在但已永久刪除
422Unprocessable Entity語法正確但語意無效(違反業務規則)
429Too Many Requests超過速率限制;須包含 Retry-After 標頭
500Internal Server Error非預期的伺服器端失敗
503Service Unavailable暫時過載或維護中;須包含 Retry-After 標頭

400 與 422 的差異很重要:400 用於結構上格式錯誤的請求(無法解析的 JSON、錯誤的 content type);422 用於可正常解析但違反業務規則或語意約束的請求。

RFC 9457 問題詳情格式 ​

RFC 9457 定義了 application/problem+json 媒體類型。問題詳情物件是一個 JSON 物件,包含五個標準成員。規範中所有成員都是可選的,但實務上 type、title、status 和 detail 都應該提供。

欄位類型說明
typeURI識別問題類型。應解析為人類可讀的文件。使用你自己網域下的穩定 URL。
titlestring問題類型的簡短人類可讀摘要。在不同發生情境間不應改變。
statusintegerHTTP 狀態碼。為方便起見而包含(客戶端應以實際 HTTP 狀態為準)。
detailstring對此次具體發生的人類可讀說明。可以顯示給終端使用者。
instanceURI識別此次具體發生的 URI 參考。可以是關聯 ID 或日誌 URL。

允許擴充:可以根據需要新增任意成員(例如用於欄位級驗證詳情的 errors,或用於追蹤的 correlation_id)。

問題詳情回應的媒體類型為:

Content-Type: application/problem+json

驗證錯誤:欄位級詳情 ​

當請求驗證失敗時,400 回應必須指出哪些欄位無效以及原因。通用的「無效請求」回應會迫使客戶端開發者猜測,或發送第二張支援工單。

使用 errors 陣列擴充標準問題詳情物件:

json
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/validation-error",
  "title": "Validation Error",
  "status": 400,
  "detail": "The request contains invalid fields.",
  "instance": "/requests/a3f5d812-...",
  "errors": [
    {
      "field": "email",
      "code": "INVALID_FORMAT",
      "message": "Must be a valid email address."
    },
    {
      "field": "due_date",
      "code": "DATE_IN_PAST",
      "message": "due_date must be a future date."
    }
  ]
}

每個錯誤項目中的 code 欄位是機器可讀且在 API 版本之間穩定的。客戶端程式碼可以對 errors[n].code 進行 switch,而不需要解析人類可讀的 message。

業務邏輯錯誤:語意失敗 ​

業務規則違反使用 409 Conflict 或 422 Unprocessable Entity。type URI 識別具體問題,允許客戶端以程式方式處理。

json
HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/task-already-completed",
  "title": "Task Already Completed",
  "status": 409,
  "detail": "Task 42 cannot be reassigned because it was completed on 2026-03-15.",
  "instance": "/requests/b7e1c402-...",
  "task_id": 42,
  "completed_at": "2026-03-15T09:41:00Z"
}

擴充欄位 task_id 和 completed_at 為客戶端提供了顯示有意義訊息或採取修正行動所需的一切,無需任何字串解析。

內部錯誤:不要洩漏堆疊追蹤 ​

當發生非預期錯誤時,伺服器必須回傳 500 Internal Server Error。回應中不得包含:

  • 堆疊追蹤
  • SQL 查詢或 ORM 錯誤訊息
  • 內部檔案路徑或類別名稱
  • 資料庫連接字串或憑證
  • 暴露拓撲的第三方服務錯誤詳情

取而代之,回傳包含關聯 ID 的最小問題詳情物件。關聯 ID 是客戶端看到的內容與伺服器日誌之間的橋樑。

json
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/internal-error",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "An unexpected error occurred. Use the correlation_id to report this issue.",
  "correlation_id": "req-7f3a9b21-4e2d-11ef-8c3a-0a9b1c2d3e4f"
}

在內部記錄完整錯誤(例外、堆疊追蹤、請求上下文),以相同的 correlation_id 作為索引鍵。當客戶端回報錯誤時,關聯 ID 是找到完整診斷軌跡所需的唯一資訊。結構化日誌慣例請參見 BEE-14002。

關聯 ID ​

每個請求都應在 API 閘道或入口點分配一個唯一的關聯 ID。該 ID 必須:

  • 傳播至所有下游服務呼叫
  • 包含在該請求的所有日誌項目中
  • 在錯誤回應中回傳(作為 correlation_id 或在 instance URI 中)

當客戶端收到錯誤時,他們將關聯 ID 附加到錯誤報告中。值班工程師使用該 ID 查詢日誌,立即看到完整的情況,無需猜測,也不需要在多個服務之間進行日誌考古。

X-Correlation-ID: req-7f3a9b21-4e2d-11ef-8c3a-0a9b1c2d3e4f

部分 API 使用 X-Request-ID 或將 ID 嵌入 instance 欄位。具體機制不如保證重要:每個錯誤回應都包含可追蹤的識別符。

速率限制:Retry-After ​

429 Too Many Requests 必須包含 Retry-After 標頭。沒有它,客戶端就沒有依據選擇重試間隔,要麼任意退避(糟糕的使用者體驗),要麼立即再次請求(加劇問題)。

HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 30

{
  "type": "https://api.example.com/problems/rate-limit-exceeded",
  "title": "Rate Limit Exceeded",
  "status": 429,
  "detail": "You have exceeded 100 requests per minute. Retry after 30 seconds.",
  "retry_after": 30,
  "limit": 100,
  "window": "60s"
}

Retry-After 接受整數秒數或 HTTP 日期格式。使用整數秒數較為簡單。

機器可讀 vs 人類可讀錯誤 ​

每個錯誤回應同時服務兩類受眾:

受眾欄位需求
客戶端程式碼type、errors[].code、status穩定、版本化、可文件化
人類開發者title、detail、errors[].message清晰、具體、可採取行動
支援 / 值班人員correlation_id、instance唯一、可追蹤

type URI 和錯誤 code 值是機器可讀的契約。一旦發佈後就不得更改(將它們視為 API 路徑)。detail 字串和 message 字串是人類可讀的,可以隨時間本地化或改進。

圖示 ​

範例 ​

正確做法:RFC 9457 驗證錯誤 ​

POST /tasks
Content-Type: application/json

{
  "title": "",
  "due_date": "2020-01-01"
}

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
X-Correlation-ID: req-a1b2c3d4

{
  "type": "https://api.example.com/problems/validation-error",
  "title": "Validation Error",
  "status": 400,
  "detail": "The request body contains 2 invalid fields.",
  "instance": "/requests/req-a1b2c3d4",
  "errors": [
    {
      "field": "title",
      "code": "REQUIRED_FIELD_EMPTY",
      "message": "title must not be blank."
    },
    {
      "field": "due_date",
      "code": "DATE_IN_PAST",
      "message": "due_date must be a future date; received 2020-01-01."
    }
  ]
}

正確做法:RFC 9457 業務邏輯錯誤 ​

POST /tasks/42/complete
Content-Type: application/json

HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/task-already-completed",
  "title": "Task Already Completed",
  "status": 409,
  "detail": "Task 42 is already in state 'completed' and cannot transition again.",
  "instance": "/requests/req-b5e7f001",
  "task_id": 42,
  "current_state": "completed"
}

錯誤做法:body 中使用 200 回傳錯誤(反模式) ​

POST /tasks
Content-Type: application/json

HTTP/1.1 200 OK
Content-Type: application/json

{
  "success": false,
  "error": "Something went wrong",
  "code": -1
}

此模式的問題:

  • HTTP 狀態 200 告訴每個代理、監控和客戶端函式庫請求已成功。
  • code: -1 對客戶端毫無意義;沒有穩定的契約。
  • 沒有關聯 ID、沒有欄位詳情、沒有可採取行動的指引。
  • error 字串不穩定,任何措辭更改都會破壞客戶端解析。

錯誤做法:洩漏內部詳情(反模式) ​

json
HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{
  "error": "NullPointerException at com.example.service.TaskService.complete(TaskService.java:142)",
  "caused_by": "org.postgresql.util.PSQLException: ERROR: deadlock detected",
  "stack": "com.example.service.TaskService.complete(TaskService.java:142)\n  com.example.controller..."
}

這暴露了類別名稱、行號、資料庫類型和查詢失敗詳情。攻擊者可以利用這些資訊對程式碼庫進行映射並識別注入目標。

常見錯誤 ​

1. 對錯誤回傳 200

最具破壞性的反模式。每個 HTTP 感知工具(監控儀表板、負載平衡器健康檢查、客戶端重試函式庫)都依賴狀態碼。對錯誤回傳 200 會破壞整個基礎設施所依賴的訊號。

2. 在正式環境洩漏堆疊追蹤

API 回應中的堆疊追蹤暴露了內部類別結構、資料庫查詢、基礎設施拓撲,有時甚至是憑證。對 API 使用者(他們無法修復伺服器程式碼)幾乎沒有幫助,但對攻擊者卻提供了巨大的便利。

3. 跨端點錯誤形狀不一致

當每個端點各自發明錯誤格式({"message": ...} vs {"error": ...} vs {"errors": [...]})時,客戶端開發者必須為每個端點編寫特定的解析程式碼。統一使用 application/problem+json 形狀意味著一條錯誤處理路徑可以處理所有情況。

4. 沒有關聯 ID

沒有關聯 ID,跨服務邊界追蹤錯誤需要在多個日誌串流中比對時間戳,這個過程最好情況下需要幾分鐘,在時鐘偏差時更會失敗。關聯 ID 讓這個過程變得即時。

5. 沒有可採取行動詳情的通用訊息

「Something went wrong」或「Internal error」對開發者毫無幫助。對於 4xx 錯誤,至少應告知:什麼是錯的、是哪個欄位或參數造成的、有效值是什麼樣的。對於 5xx 錯誤,提供關聯 ID 讓他們可以提交有意義的支援工單。

相關 BEE ​

參考資料 ​