template-monorepo/internal/library/errors/README.md

10 KiB
Raw Permalink Blame History

結構化錯誤碼

套件路徑:gateway/internal/library/errorsimport 建議別名 errs

錯誤碼格式

8 碼 SSCCCDDD(十進位,左側補零顯示):

名稱 範圍 說明
SS Scope 0099 服務 / 模組(見 code/types.go 常數)
CCC Category 000999 錯誤類別,決定 HTTP / gRPC 映射
DDD Detail 000999 業務細節,不影響 HTTP 狀態(CatGRPC 除外)

範例:10101000 → Scope=10Facade、Category=101InputInvalidFormat、Detail=000

10101000
^^       Scope = 10
   ^^^    Category = 101
      ^^^ Detail = 000

快速開始

package myhandler

import (
    "net/http"

    errs "gateway/internal/library/errors"
    "gateway/internal/library/errors/code"
)

// 模組頂層綁定 scope每個 binary / 套件一次)
var errb = errs.For(code.Facade)

func GetUser(w http.ResponseWriter, id string) error {
    if id == "" {
        return errb.InputInvalidFormat("缺少參數: id")
    }

    u, err := repo.Find(id)
    if err != nil {
        return errb.DBError("查詢失敗").WithCause(err)
    }
    if u == nil {
        return errb.ResNotFound("user", id)
    }

    return nil
}

func writeHTTP(w http.ResponseWriter, e *errs.Error) {
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.Header().Set("X-Error-Code", e.DisplayCode())
    w.WriteHeader(e.HTTPStatus())
    // body: {"code":"10101000","message":"..."}
}

常用 API

需求 用法
綁定 scope errs.For(code.Facade)
語意化建構 errb.ResNotFound("user", id)
自訂 category + detail errb.Code(code.SysTimeout, 42, "下游逾時")
附加底層錯誤 e.WithCause(err)(不可變)
更換 scope e.WithScope(code.LocalAPI)
嚴格驗證 errs.New(scope, cat, det, msg)(*Error, error)
從 8 碼還原 errs.FromCode(10101000)
日誌(可選) gateway/internal/library/errlog + slog

Category → HTTP 完整對照表

HTTPStatus() 實作一致。未列出的 category 會 fallback 為 500

A. Input1xx

Category 常數 HTTP 說明
101 InputInvalidFormat 400 Bad Request 格式錯、型別錯
102 InputNotValidImplementation 422 Unprocessable Entity 語意正確但無法依目前實作處理
103 InputInvalidRange 422 數值 / 範圍不合法
104 InputMissingRequired 400 必填欄位缺失
105 InputUnsupportedMedia 415 Unsupported Media Type Content-Type 不支援
106 InputPayloadTooLarge 413 Payload Too Large HTTP body 過大

B. DB2xx

Category 常數 HTTP 說明
201 DBError 500 Internal Server Error 非預期 DB 故障
202 DBDataConvert 422 可修正的資料轉換問題
203 DBDuplicate 409 Conflict 唯一鍵 / 重複寫入
204 DBUnavailable 503 Service Unavailable DB 暫時不可用(可重試)

C. Resource3xx

Category 常數 HTTP 說明
301 ResNotFound 404 Not Found 資源不存在
302 ResInvalidFormat 422 資源表示 / Schema 不符
303 ResAlreadyExist 409 已存在
304 ResInsufficient 400 數量 / 容量不足(客戶端可調參數)
305 ResInsufficientPerm 403 Forbidden 對該資源無權限
306 ResInvalidMeasureID 400 ID 格式不合法
307 ResExpired 410 Gone 已過期
308 ResMigrated 410 已遷移(可在 Gateway 加 Location
309 ResInvalidState 409 狀態機不允許此操作
310 ResInsufficientQuota 429 Too Many Requests 配額 / 額度不足
311 ResMultiOwner 409 所有權衝突
312 ResPreconditionFailed 412 Precondition Failed ETag / 版本前置條件失敗
313 ResLocked 423 Locked 資源被鎖定

D. gRPC 轉換4xx

Category 常數 HTTP 說明
400 CatGRPC 依 Detail Detail 存標準 codes.Code016見下方子表

CatGRPC 子映射(與 Google API 設計指南 對齊):

gRPC Code HTTP
InvalidArgument, OutOfRange, FailedPrecondition 400
NotFound 404
AlreadyExists, Aborted 409
PermissionDenied 403
Unauthenticated 401
ResourceExhausted 429
DeadlineExceeded 504
Unavailable 503
Unimplemented 501
Canceled 408
Internal, Unknown, DataLoss 500

E. Auth5xx

Category 常數 HTTP 說明
501 AuthUnauthorized 401 Unauthorized 未提供或無效憑證
502 AuthExpired 401 Token / 會話過期
503 AuthInvalidPosixTime 401 時戳異常導致驗簽失敗
504 AuthSigPayloadMismatch 401 簽名與 payload 不符
505 AuthForbidden 403 已驗證但無操作權限
506 AuthMethodNotAllowed 405 Method Not Allowed HTTP method 不允許

F. System6xx

Category 常數 HTTP 說明
601 SysInternal 500 系統內部錯誤
602 SysMaintain 503 Service Unavailable 維護 / 停機
603 SysTimeout 504 Gateway Timeout 處理或下游逾時
604 SysTooManyRequest 429 全局限流
605 SysNotImplemented 501 Not Implemented 功能未上線 / 開關關閉
606 SysClientTimeout 408 Request Timeout 客戶端未完成請求(與 603/504 區分)

G. PubSub7xx

Category 常數 HTTP 說明
701 PSuPublish 502 Bad Gateway 發佈到匯流排失敗
702 PSuConsume 502 消費失敗
703 PSuTooLarge 413 Payload Too Large 訊息過大

H. Service8xx

Category 常數 HTTP 說明
801 SvcInternal 500 服務邏輯內錯
802 SvcThirdParty 502 呼叫外部 API 失敗
803 SvcHTTP400 400 明確要回 400 的業務情境
804 SvcMaintenance 503 模組級維護
805 SvcRateLimited 429 特定下游 / 供應商限流

HTTP 狀態碼使用一覽

本套件目前會回傳的 HTTP 狀態:

200 400 401 403 404 405 408 409 410 412 413 415 422 423 429 500 501 502 503 504


HTTP 映射設計評估

整體結論:合理,可作為 API Gateway 的預設策略

多數映射符合 REST 慣例與 Google / Microsoft API 設計指南。Category 負責「協定層語意」Detail 負責「業務細節」,分工清楚。

映射得當(建議維持)

映射 理由
Input 格式 → 400語意 → 422 區分「語法錯」與「語意錯」,利於客戶端處理
DB 重複 → 409 標準 REST 做法
資源不存在 → 404 標準
過期 / 遷移 → 410 比 404 更精確表達「曾存在但不可用」
狀態衝突 / 多所有者 → 409 符合狀態機與併發場景
配額 / 限流 → 429 Retry-After 搭配良好
未驗證 → 401已驗證無權 → 403 符合 RFC 9110
下游 / 第三方 / MQ → 502 Gateway 視角正確
維護 → 503 可搭配 Retry-After
處理逾時 → 504 適合 Gateway區分於客戶端 408

可商榷(依產品政策調整,非必須改)

現狀 替代方案 何時考慮
DBError → 500 使用 DBUnavailable → 503 暫時性連線問題應改用 errb.DBUnavailable()
ResInsufficient → 400 409422 若語意是「庫存不足導致無法完成訂單」而非「參數錯」
PSuPublish/Consume → 502 503 訊息中介明確處於不可用(非單次請求失敗)
SysTimeout → 504 500 若逾時發生在服務內部、前面沒有 Gateway 代理
AuthExpired → 401 少數團隊用 403 401 較符合 OAuth2 / OIDC 慣例,建議維持 401
FailedPreconditiongRPC→ 400 412 若錯誤來自 If-Match / ETag 版本衝突

先前缺口(已補)

問題 處理
CatGRPC400未映射一律 500 已依 Detail 中的 gRPC code 映射 HTTP見上表
未知 category 仍 fallback 500(保守、安全)

號段保留建議(擴充用)

號段 用途
104199 Input 擴充
204299 DB 擴充
312399 Resource 擴充400 保留給 CatGRPC
506599 Auth 擴充
605699 System 擴充
704799 PubSub 擴充
805899 Service 擴充
900999 平台 / 保留

gRPC

// 服務端:回傳 status
return e.GRPCStatus().Err()

// 客戶端:還原為 *Error內建 gRPC 碼請傳入本服務 scope
e, err := errs.FromGRPCError(grpcErr, code.Facade)
  • 業務 8 碼寫在 message[10101007] email invalid
  • GRPCCode()Category 映射標準 gRPC code不會把 8 碼當成 gRPC code
  • Category → gRPC 對照見 grpc.gogrpcCodeForCategory

日誌(可選,errlog

import (
    "log/slog"

    "gateway/internal/library/errlog"
)

errlog.Error(ctx, slog.Default(), e, "request failed", "req_id", reqID)

// 或只取欄位,餵給 go-zero / zap
attrs := errlog.Attrs(e)

Scope 常數

定義於 code/types.go,例如:Facade(10)handler parseAuth(28)Member(29)Notification(30)GearAssetMgr(27)
新增服務時在該檔登記,避免號段衝突。


遷移(舊版 API

errs.Scope = code.Facade var errb = errs.For(code.Facade)
errs.ResNotFoundError("x") errb.ResNotFound("x")
e.Wrap(err) e.WithCause(err)
ResNotFoundErrorL / WithLog 先建構 errorerrlog.Error

測試

go test ./internal/library/errors/...