backend/pkg/library/errors/errors.go

234 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}