2026-06-15 19:17:09 +08:00

108 lines
2.5 KiB
Go
Raw Permalink 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 {
return SpecOf(code).GRPCCode
}
// 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, codes.DeadlineExceeded:
// 下游服务未启动、网络不可达或 gRPC deadline 到期都属于依赖暂不可用gateway 应返回 UPSTREAM_ERROR
// 不能降级为 INTERNAL_ERROR否则本地联调和线上排障都会误判成业务代码内部崩溃。
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})
}