結構化錯誤碼
套件路徑:gateway/internal/library/errors(import 建議別名 errs)
錯誤碼格式
8 碼 SSCCCDDD(十進位,左側補零顯示):
| 段 |
名稱 |
範圍 |
說明 |
| SS |
Scope |
00–99 |
服務 / 模組(見 code/types.go 常數) |
| CCC |
Category |
000–999 |
錯誤類別,決定 HTTP / gRPC 映射 |
| DDD |
Detail |
000–999 |
業務細節,不影響 HTTP 狀態(CatGRPC 除外) |
範例:10101000 → Scope=10(Facade)、Category=101(InputInvalidFormat)、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. Input(1xx)
| 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. DB(2xx)
| Category |
常數 |
HTTP |
說明 |
| 201 |
DBError |
500 Internal Server Error |
非預期 DB 故障 |
| 202 |
DBDataConvert |
422 |
可修正的資料轉換問題 |
| 203 |
DBDuplicate |
409 Conflict |
唯一鍵 / 重複寫入 |
| 204 |
DBUnavailable |
503 Service Unavailable |
DB 暫時不可用(可重試) |
C. Resource(3xx)
| 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.Code(0–16),見下方子表 |
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. Auth(5xx)
| 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. System(6xx)
| 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. PubSub(7xx)
| Category |
常數 |
HTTP |
說明 |
| 701 |
PSuPublish |
502 Bad Gateway |
發佈到匯流排失敗 |
| 702 |
PSuConsume |
502 |
消費失敗 |
| 703 |
PSuTooLarge |
413 Payload Too Large |
訊息過大 |
H. Service(8xx)
| 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 |
409 或 422 |
若語意是「庫存不足導致無法完成訂單」而非「參數錯」 |
PSuPublish/Consume → 502 |
503 |
訊息中介明確處於不可用(非單次請求失敗) |
SysTimeout → 504 |
500 |
若逾時發生在服務內部、前面沒有 Gateway 代理 |
AuthExpired → 401 |
少數團隊用 403 |
401 較符合 OAuth2 / OIDC 慣例,建議維持 401 |
FailedPrecondition(gRPC)→ 400 |
412 |
若錯誤來自 If-Match / ETag 版本衝突 |
先前缺口(已補)
| 問題 |
處理 |
CatGRPC(400)未映射,一律 500 |
已依 Detail 中的 gRPC code 映射 HTTP(見上表) |
| 未知 category |
仍 fallback 500(保守、安全) |
號段保留建議(擴充用)
| 號段 |
用途 |
| 104–199 |
Input 擴充 |
| 204–299 |
DB 擴充 |
| 312–399 |
Resource 擴充(400 保留給 CatGRPC) |
| 506–599 |
Auth 擴充 |
| 605–699 |
System 擴充 |
| 704–799 |
PubSub 擴充 |
| 805–899 |
Service 擴充 |
| 900–999 |
平台 / 保留 |
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.go 中 grpcCodeForCategory
日誌(可選,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)、LocalAPI(11)、GearAuditLog(12) … 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 |
先建構 error,再 errlog.Error |
測試
go test ./internal/library/errors/...