234 lines
6.4 KiB
Go
234 lines
6.4 KiB
Go
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 // 400:ID 無效
|
||
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
|
||
}
|