backend/pkg/library/errors/errors.go

234 lines
6.4 KiB
Go
Raw Normal View History

2025-11-04 09:47:36 +00:00
package errs
import (
"errors"
"fmt"
"net/http"
"backend/pkg/library/errors/code"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// Scope is a global variable that should be set by the service or module.
var Scope = code.Unset
// Error represents a structured error with an 8-digit code.
// The code is composed of a 2-digit scope, a 3-digit category, and a 3-digit detail.
// Format: SSCCCDDD
type Error struct {
scope uint32 // 2-digit service scope
category uint32 // 3-digit category
detail uint32 // 3-digit detail
msg string // Display message for the client
internalErr error // The actual underlying error
}
// New creates a new Error.
// It ensures that category is within 0-999 and detail is within 0-999.
func New(scope, category, detail uint32, displayMsg string) *Error {
if category > uint32(code.MaxCategory) {
category = uint32(code.ReservedMaxCategory)
}
if detail > uint32(code.MaxDetail) {
detail = uint32(code.ReservedMaxDetail)
}
return &Error{
scope: scope,
category: category,
detail: detail,
msg: displayMsg,
}
}
// Error returns the display message. This is intended for the client.
// For internal logging and debugging, use Unwrap() to get the underlying error.
func (e *Error) Error() string {
if e == nil {
return ""
}
return e.msg
}
// Scope returns the 2-digit scope of the error.
func (e *Error) Scope() uint32 {
if e == nil {
return uint32(code.Unset)
}
return e.scope
}
// Category returns the 3-digit category of the error.
func (e *Error) Category() uint32 {
if e == nil {
return uint32(code.DefaultCategory)
}
return e.category
}
// Detail returns the 2-digit detail code of the error.
func (e *Error) Detail() uint32 {
if e == nil {
return uint32(code.DefaultDetail)
}
return e.detail
}
// SubCode returns the 6-digit code (category + detail).
func (e *Error) SubCode() uint32 {
if e == nil {
return code.OK
}
c := e.category*code.CategoryMultiplier + e.detail
return c
}
// Code returns the full 8-digit error code (scope + category + detail).
func (e *Error) Code() uint32 {
if e == nil {
return code.NonCode
}
return e.Scope()*code.ScopeMultiplier + e.SubCode()
}
// DisplayCode returns the 8-digit error code as a zero-padded string.
func (e *Error) DisplayCode() string {
if e == nil {
return "00000000"
}
return fmt.Sprintf("%08d", e.Code())
}
// Is checks if the target error is of type *Error and has the same sub-code.
// It is called by errors.Is(). Do not use it directly.
func (e *Error) Is(target error) bool {
var err *Error
if !errors.As(target, &err) {
return false
}
return e.SubCode() == err.SubCode()
}
// Unwrap returns the underlying wrapped error.
func (e *Error) Unwrap() error {
if e == nil {
return nil
}
return e.internalErr
}
// Wrap sets the internal error for the current error.
func (e *Error) Wrap(internalErr error) *Error {
if e != nil {
e.internalErr = internalErr
}
return e
}
// GRPCStatus converts the error to a gRPC status.
func (e *Error) GRPCStatus() *status.Status {
if e == nil {
return status.New(codes.OK, "")
}
return status.New(codes.Code(e.Code()), e.Error())
}
// HTTPStatus returns the corresponding HTTP status code for the error.
func (e *Error) HTTPStatus() int {
if e == nil || e.SubCode() == code.OK {
return http.StatusOK
}
switch e.Category() {
// Input
case uint32(code.InputInvalidFormat):
return http.StatusBadRequest // 400輸入格式錯
case uint32(code.InputNotValidImplementation),
uint32(code.InputInvalidRange):
return http.StatusUnprocessableEntity // 422語意正確但無法處理範圍/實作)
// DB
case uint32(code.DBError):
return http.StatusInternalServerError // 500後端暫時性故障若你偏好 503 可自行調整)
case uint32(code.DBDataConvert):
return http.StatusUnprocessableEntity // 422可修正的資料轉換失敗
case uint32(code.DBDuplicate):
return http.StatusConflict // 409唯一鍵/重複
// Resource
case uint32(code.ResNotFound):
return http.StatusNotFound // 404資源不存在
case uint32(code.ResInvalidFormat):
return http.StatusUnprocessableEntity // 422資源表示/格式不符
case uint32(code.ResAlreadyExist):
return http.StatusConflict // 409已存在
case uint32(code.ResInsufficient):
return http.StatusBadRequest // 400數量/容量/條件不足(可由客戶端修正)
case uint32(code.ResInsufficientPerm):
return http.StatusForbidden // 403資源層面的權限不足
case uint32(code.ResInvalidMeasureID):
return http.StatusBadRequest // 400ID 無效
case uint32(code.ResExpired):
return http.StatusGone // 410資源已過期/不可用
case uint32(code.ResMigrated):
return http.StatusGone // 410已遷移若需導引可由上層加 Location
case uint32(code.ResInvalidState):
return http.StatusConflict // 409目前狀態不允許此操作
case uint32(code.ResInsufficientQuota):
return http.StatusTooManyRequests // 429配額不足/達上限
case uint32(code.ResMultiOwner):
return http.StatusConflict // 409多所有者衝突
// Auth
case uint32(code.AuthUnauthorized),
uint32(code.AuthExpired),
uint32(code.AuthInvalidPosixTime),
uint32(code.AuthSigPayloadMismatch):
return http.StatusUnauthorized // 401未驗證/無效憑證
case uint32(code.AuthForbidden):
return http.StatusForbidden // 403有身分但沒權限
// System
case uint32(code.SysTooManyRequest):
return http.StatusTooManyRequests // 429節流
case uint32(code.SysInternal):
return http.StatusInternalServerError // 500系統內部錯
case uint32(code.SysMaintain):
return http.StatusServiceUnavailable // 503維護中
case uint32(code.SysTimeout):
return http.StatusGatewayTimeout // 504處理/下游逾時
// PubSub
case uint32(code.PSuPublish),
uint32(code.PSuConsume):
return http.StatusBadGateway // 502訊息中介/外部匯流排失敗
case uint32(code.PSuTooLarge):
return http.StatusRequestEntityTooLarge // 413訊息太大
// Service
case uint32(code.SvcMaintenance):
return http.StatusServiceUnavailable // 503服務維護
case uint32(code.SvcInternal):
return http.StatusInternalServerError // 500服務內部錯
case uint32(code.SvcThirdParty):
return http.StatusBadGateway // 502第三方依賴失敗
case uint32(code.SvcHTTP400):
return http.StatusBadRequest // 400明確指派 400
}
// fallback
return http.StatusInternalServerError
}