2026-05-08 18:48:29 +08:00

129 lines
3.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 xerr
import (
"errors"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const errorDomain = "hyapp"
// GRPCCode 把稳定业务 reason 映射为 gRPC 标准 code。
// gateway 后续只需要读取 gRPC code 和 ErrorInfo.reason 即可生成 HTTP envelope。
func GRPCCode(code Code) codes.Code {
switch code {
case InvalidArgument, DisplayUserIDInvalid, InvalidInviteCode, InvalidSection, PageTokenInvalid:
return codes.InvalidArgument
case NotFound, DisplayUserIDNotFound, CountryNotFound, RegionNotFound, MessageNotFound, MessageRecalled:
return codes.NotFound
case Conflict, RoomClosed, RegionCountryConflict, RegionDisabled, RequestConflict, ProducerEventConflict:
return codes.FailedPrecondition
case Unauthorized, AuthFailed, SessionExpired, SessionRevoked:
return codes.Unauthenticated
case PermissionDenied, ProfileRequired, UserDisabled:
return codes.PermissionDenied
case Unavailable:
return codes.Unavailable
case PasswordAlreadySet, DisplayUserIDExists, DuplicateBillingCommand, EventAlreadyConsumed:
return codes.AlreadyExists
case DisplayUserIDCooldown, DisplayUserIDPrettyNotAvailable, DisplayUserIDPrettyActive, DisplayUserIDLeaseExpired, DisplayUserIDPaymentRequired, CountryChangeCooldown, InsufficientBalance, RuleNotActive, RewardPending:
return codes.FailedPrecondition
case DisplayUserIDAllocateFailed:
return codes.Internal
case LedgerConflict:
return codes.Aborted
default:
return codes.Internal
}
}
// ToGRPCError 只在 transport/grpc 层调用。
// 如果传入的错误已经是 gRPC status会原样返回避免重复包裹健康检查等基础错误。
func ToGRPCError(err error) error {
if err == nil {
return nil
}
if _, ok := status.FromError(err); ok {
return err
}
domainErr, ok := As(err)
if !ok {
domainErr = &Error{Code: Internal, Message: "internal error"}
}
st := status.New(GRPCCode(domainErr.Code), domainErr.Message)
detail := &errdetails.ErrorInfo{
Reason: string(domainErr.Code),
Domain: errorDomain,
}
withDetails, detailErr := st.WithDetails(detail)
if detailErr != nil {
return st.Err()
}
return withDetails.Err()
}
// ReasonFromGRPC 供 gateway 或测试从 gRPC error detail 中恢复稳定 reason。
func ReasonFromGRPC(err error) Code {
if err == nil {
return ""
}
st, ok := status.FromError(err)
if !ok {
return Internal
}
for _, detail := range st.Details() {
info, ok := detail.(*errdetails.ErrorInfo)
if ok && info.GetReason() != "" {
return Code(info.GetReason())
}
}
return codeFromGRPC(st.Code())
}
// codeFromGRPC 是没有 ErrorInfo detail 时的降级映射。
func codeFromGRPC(code codes.Code) Code {
switch code {
case codes.InvalidArgument:
return InvalidArgument
case codes.NotFound:
return NotFound
case codes.Unauthenticated:
return Unauthorized
case codes.PermissionDenied:
return PermissionDenied
case codes.Unavailable:
return Unavailable
case codes.AlreadyExists, codes.FailedPrecondition, codes.Aborted:
return Conflict
default:
return Internal
}
}
// IsCode 帮助测试或 transport 分支判断错误 reason。
func IsCode(err error, code Code) bool {
if err == nil {
return false
}
if typed, ok := As(err); ok {
return typed.Code == code
}
if _, ok := status.FromError(err); ok {
return ReasonFromGRPC(err) == code
}
return errors.Is(err, &Error{Code: code})
}