backend/pkg/library/errors/README.md

186 lines
7.3 KiB
Markdown
Raw Permalink Normal View History

2025-11-04 09:47:36 +00:00
# 錯誤碼 × HTTP 對照表
這份文件專門整理 **infra-core/errors** 的「錯誤碼 → HTTP Status」對照並提供**實務範例**。
錯誤系統採用 8 碼格式 `SSCCCDDD`
- `SS` = Scope服務/模組,兩位數)
- `CCC` = Category類別三位數影響 HTTP 狀態)
- `DDD` = Detail細節三位數自定義業務碼
> 例如:`10101000` → Scope=10、Category=101InputInvalidFormat、Detail=000。
## 目錄
- [1) 快速查表](#1-快速查表依類別整理)
- [2) 使用範例](#2-使用範例)
- [3) 小撇步與慣例](#3-小撇步與慣例)
- [4) 安裝與測試](#4-安裝與測試)
- [5) 變更日誌](#5-變更日誌)
---
## 1) 快速查表(依類別整理)
### A. InputCategory 1xx
| Category 常數 | 說明 | HTTP | 原因/說明 |
|---|---------------|:----:|---|
| `InputInvalidFormat` (101) | 無效格式 | **400 Bad Request** | 格式不符、缺欄位、型別錯。 |
| `InputNotValidImplementation` (102) | 非有效實作 | **422 Unprocessable Entity** | 語意正確但無法處理。 |
| `InputInvalidRange` (103) | 無效範圍 | **422 Unprocessable Entity** | 值超域、邊界條件不合。 |
### B. DBCategory 2xx
| Category 常數 | 說明 | HTTP | 原因/說明 |
|---|-------------|:----:|---|
| `DBError` (201) | 資料庫一般錯誤 | **500 Internal Server Error** | 後端故障/不可預期。 |
| `DBDataConvert` (202) | 資料轉換錯誤 | **422 Unprocessable Entity** | 可修正的資料問題(格式/型別轉換失敗)。 |
| `DBDuplicate` (203) | 資料重複 | **409 Conflict** | 唯一鍵衝突、重複建立。 |
### C. ResourceCategory 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. AuthCategory 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. SystemCategory 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. PubSubCategory 7xx
| Category 常數 | 說明 | HTTP | 原因/說明 |
|---|---------|:----:|---|
| `PSuPublish` (701) | 發佈失敗 | **502 Bad Gateway** | 中介或外部匯流排錯誤。 |
| `PSuConsume` (702) | 消費失敗 | **502 Bad Gateway** | 同上。 |
| `PSuTooLarge` (703) | 訊息過大 | **413 Payload Too Large** | 封包大小超限。 |
### G. ServiceCategory 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
```