# 錯誤碼 × HTTP 對照表 這份文件專門整理 **infra-core/errors** 的「錯誤碼 → HTTP Status」對照,並提供**實務範例**。 錯誤系統採用 8 碼格式 `SSCCCDDD`: - `SS` = Scope(服務/模組,兩位數) - `CCC` = Category(類別,三位數,影響 HTTP 狀態) - `DDD` = Detail(細節,三位數,自定義業務碼) > 例如:`10101000` → Scope=10、Category=101(InputInvalidFormat)、Detail=000。 ## 目錄 - [1) 快速查表](#1-快速查表依類別整理) - [2) 使用範例](#2-使用範例) - [3) 小撇步與慣例](#3-小撇步與慣例) - [4) 安裝與測試](#4-安裝與測試) - [5) 變更日誌](#5-變更日誌) --- ## 1) 快速查表(依類別整理) ### A. Input(Category 1xx) | Category 常數 | 說明 | HTTP | 原因/說明 | |---|---------------|:----:|---| | `InputInvalidFormat` (101) | 無效格式 | **400 Bad Request** | 格式不符、缺欄位、型別錯。 | | `InputNotValidImplementation` (102) | 非有效實作 | **422 Unprocessable Entity** | 語意正確但無法處理。 | | `InputInvalidRange` (103) | 無效範圍 | **422 Unprocessable Entity** | 值超域、邊界條件不合。 | ### B. DB(Category 2xx) | Category 常數 | 說明 | HTTP | 原因/說明 | |---|-------------|:----:|---| | `DBError` (201) | 資料庫一般錯誤 | **500 Internal Server Error** | 後端故障/不可預期。 | | `DBDataConvert` (202) | 資料轉換錯誤 | **422 Unprocessable Entity** | 可修正的資料問題(格式/型別轉換失敗)。 | | `DBDuplicate` (203) | 資料重複 | **409 Conflict** | 唯一鍵衝突、重複建立。 | ### C. Resource(Category 3xx) | Category 常數 | 說明 | HTTP | 原因/說明 | |---|-------------------|:----:|---| | `ResNotFound` (301) | 資源未找到 | **404 Not Found** | 目標不存在/無此 ID。 | | `ResInvalidFormat` (302) | 無效資源格式 | **422 Unprocessable Entity** | 表示層/Schema 不符。 | | `ResAlreadyExist` (303) | 資源已存在 | **409 Conflict** | 重複建立/命名衝突。 | | `ResInsufficient` (304) | 資源不足 | **400 Bad Request** | 數量/容量不足(用戶可改參數再試)。 | | `ResInsufficientPerm` (305) | 權限不足 | **403 Forbidden** | 已驗證但無權限。 | | `ResInvalidMeasureID` (306) | 無效測量ID | **400 Bad Request** | ID 本身不合法。 | | `ResExpired` (307) | 資源過期 | **410 Gone** | 已不可用(可於上層補 Location)。 | | `ResMigrated` (308) | 資源已遷移 | **410 Gone** | 同上,如需導引請於上層處理。 | | `ResInvalidState` (309) | 無效狀態 | **409 Conflict** | 當前狀態不允許此操作。 | | `ResInsufficientQuota` (310) | 配額不足 | **429 Too Many Requests** | 達配額/速率限制。 | | `ResMultiOwner` (311) | 多所有者 | **409 Conflict** | 所有權歧異造成衝突。 | ### D. Auth(Category 5xx) | Category 常數 | 說明 | HTTP | 原因/說明 | |---|-------------------------|:----:|---| | `AuthUnauthorized` (501) | 未授權/未驗證 | **401 Unauthorized** | 缺 Token、無效 Token。 | | `AuthExpired` (502) | 授權過期 | **401 Unauthorized** | Token 過期或時效失效。 | | `AuthInvalidPosixTime` (503) | 無效 POSIX 時間 | **401 Unauthorized** | 時戳異常導致驗簽失敗。 | | `AuthSigPayloadMismatch` (504) | 簽名與載荷不符 | **401 Unauthorized** | 驗簽失敗。 | | `AuthForbidden` (505) | 禁止存取 | **403 Forbidden** | 已驗證但沒有操作權限。 | ### E. System(Category 6xx) | Category 常數 | 說明 | HTTP | 原因/說明 | |---|---------------|:----:|---| | `SysInternal` (601) | 系統內部錯誤 | **500 Internal Server Error** | 未預期的系統錯。 | | `SysMaintain` (602) | 系統維護中 | **503 Service Unavailable** | 維護/停機。 | | `SysTimeout` (603) | 系統超時 | **504 Gateway Timeout** | 下游/處理逾時。 | | `SysTooManyRequest` (604) | 請求過多 | **429 Too Many Requests** | 節流/限流。 | ### F. PubSub(Category 7xx) | Category 常數 | 說明 | HTTP | 原因/說明 | |---|---------|:----:|---| | `PSuPublish` (701) | 發佈失敗 | **502 Bad Gateway** | 中介或外部匯流排錯誤。 | | `PSuConsume` (702) | 消費失敗 | **502 Bad Gateway** | 同上。 | | `PSuTooLarge` (703) | 訊息過大 | **413 Payload Too Large** | 封包大小超限。 | ### G. Service(Category 8xx) | Category 常數 | 說明 | HTTP | 原因/說明 | |---|---------------|:----:|---| | `SvcInternal` (801) | 服務內部錯誤 | **500 Internal Server Error** | 非基礎設施層的內錯。 | | `SvcThirdParty` (802) | 第三方失敗 | **502 Bad Gateway** | 呼叫外部服務失敗。 | | `SvcHTTP400` (803) | 明確指派 400 | **400 Bad Request** | 自行指定。 | | `SvcMaintenance` (804) | 服務維護中 | **503 Service Unavailable** | 模組級維運中。 | --- ## 2) 使用範例 ### 2.1 在 Handler 中回傳錯誤 ```go import ( "net/http" errs "gitlab.supermicro.com/infra/infra-core/errors" "gitlab.supermicro.com/infra/infra-core/errors/code" ) func init() { errs.Scope = code.Gateway // 設定當前服務的 Scope } func GetUser(w http.ResponseWriter, r *http.Request) error { id := r.URL.Query().Get("id") if id == "" { return errs.InputInvalidFormatError("缺少參數: id") // 現在是 8 位碼 } u, err := repo.Find(r.Context(), id) switch { case errors.Is(err, repo.ErrNotFound): return errs.ResNotFoundError("user", id) case err != nil: return errs.DBErrorError("查詢使用者失敗").Wrap(err) // Wrap 內部錯誤 } // … 寫入回應 return nil } // 統一寫出 HTTP 錯誤 func writeHTTP(w http.ResponseWriter, e *errs.Error) { http.Error(w, e.Error(), e.HTTPStatus()) } ``` ### 2.2 取出 Wrap 的內部錯誤 ```go if internal := e.Unwrap(); internal != nil { log.Error("Internal error: ", internal) } ``` ### 2.3 搭配日誌裝飾器(`WithLog` / `WithLogWrap`) ```go log := logger.WithFields(errs.LogField{Key: "req_id", Val: rid}) if badInput { return errs.WithLog(log, nil, errs.InputInvalidFormatError, "email 無效") } if err := repo.Save(ctx, u); err != nil { return errs.WithLogWrap( log, []errs.LogField{{Key: "entity", Val: "user"}, {Key: "op", Val: "save"}}, errs.DBErrorError, err, "儲存失敗", ) } ``` ### 2.4 只知道 Category+Detail 的動態場景(`EL` / `ELWrap`) ```go // 依流程動態產生 return errs.EL(log, nil, code.SysTimeout, 123, "下游逾時") // 自定義 detail=123 // 或需保留 cause: return errs.ELWrap(log, nil, code.SvcThirdParty, 456, err, "金流商失敗") ``` ### 2.5 gRPC 互通 ```go // 由 *errs.Error 轉為 gRPC status st := e.GRPCStatus() // *status.Status // 客戶端收到 gRPC error → 轉回 *errs.Error e := errs.FromGRPCError(grpcErr) fmt.Println(e.DisplayCode(), e.Error()) // e.g., "10101000" "error msg" ``` ### 2.6 從 8 碼反解(`FromCode`) ```go e := errs.FromCode(10101000) // 10101000 fmt.Println(e.Scope(), e.Category(), e.Detail()) // 10, 101, 000 ```