150 lines
4.1 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 (
"context"
"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
}
// context 终止描述的是调用链生命周期,而不是业务内部故障。仓储层会直接返回
// context.Canceled/DeadlineExceeded若先走兜底 Internal主动取消的并发 sibling
// 会在服务端被误报成 INTERNAL_ERROR并把一次正常的快速失败伪装成数据库事故。
if errors.Is(err, context.Canceled) {
return status.Error(codes.Canceled, context.Canceled.Error())
}
if errors.Is(err, context.DeadlineExceeded) {
return status.Error(codes.DeadlineExceeded, context.DeadlineExceeded.Error())
}
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,
}
if withDetails, detailErr := st.WithDetails(detail); detailErr == nil {
st = withDetails
}
return &grpcError{status: st, cause: err}
}
// grpcError 对 grpc-go 暴露脱敏后的 status同时保留脱敏前的原始错误链。
// 客户端从 GRPCStatus/Error 只能看到 catalog 文案;原始链只有服务端进程内
// 通过 CauseFromGRPC 可达,不会进 wire。
type grpcError struct {
status *status.Status
cause error
}
func (e *grpcError) Error() string {
return e.status.Err().Error()
}
// GRPCStatus 让 status.FromError 和 grpc-go server 识别脱敏 status而不是降级成 Unknown。
func (e *grpcError) GRPCStatus() *status.Status {
return e.status
}
// Unwrap 保留原始错误链,供服务端 errors.Is/As 与 access 日志定位根因。
func (e *grpcError) Unwrap() error {
return e.cause
}
// CauseFromGRPC 取回 ToGRPCError 脱敏前的原始错误链,仅用于服务端日志;
// 对已经是 gRPC status 的透传错误返回 nil。禁止把返回值写回给客户端。
func CauseFromGRPC(err error) error {
var typed *grpcError
if errors.As(err, &typed) {
return typed.cause
}
return nil
}
// 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})
}