# 錯誤碼 × 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
```