108 lines
2.5 KiB
Go
108 lines
2.5 KiB
Go
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})
|
||
}
|