From ec2bf7418beaa25e620a83fe0be00fa12c11283b Mon Sep 17 00:00:00 2001 From: "Jonathan A. Sternberg" Date: Mon, 22 Jul 2024 10:01:50 -0500 Subject: [PATCH] Add support for encoding error structure for grpc errors This adds support for encoding the error structure for grpc errors. It does this by using the details to encode any wrapped errors. Each error is composed of a `Status` protobuf. The message included in that status is the default error message if no additional details are provided. The first detail is an encoding of the error if one was given. If there was no encoding possible or it wasn't possible to deserialize it, a generic error is created with the original message. Any additional details are any wrapped errors. Each wrapped error is a `Status` that follows the same encoding scheme. This allows an error to wrap multiple errors and the implementation isn't dependent on the contents. --- errgrpc/grpc.go | 204 +++++++++++++++++++++++++++++++++++++------ errgrpc/grpc_test.go | 23 +++++ 2 files changed, 201 insertions(+), 26 deletions(-) diff --git a/errgrpc/grpc.go b/errgrpc/grpc.go index e9cfce7..7a21aec 100644 --- a/errgrpc/grpc.go +++ b/errgrpc/grpc.go @@ -23,12 +23,18 @@ package errgrpc import ( "context" + "errors" "fmt" "strconv" "strings" + "github.com/containerd/typeurl/v2" + spb "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/emptypb" "github.com/containerd/errdefs" "github.com/containerd/errdefs/internal/cause" @@ -43,51 +49,108 @@ import ( // If the error is unmapped, the original error will be returned to be handled // by the regular grpc error handling stack. func ToGRPC(err error) error { - if err == nil { - return nil + if err == nil || isGRPCError(err) { + return err } - if isGRPCError(err) { - // error has already been mapped to grpc - return err + p := &spb.Status{ + Code: int32(errorCode(err)), + Message: err.Error(), + } + withDetails(p, err) + return status.FromProto(p).Err() +} + +func withDetails(p *spb.Status, err error) { + var any *anypb.Any + if m, ok := err.(proto.Message); ok { + any, _ = anypb.New(m) + } + + if any == nil { + // If we fail to marshal the details, then use a generic + // error by setting this as the empty struct. + any, _ = anypb.New(&emptypb.Empty{}) + } + + if any == nil { + // Extra protection just in case the above fails for + // some reason. + return + } + + // First detail is a serialization of the current error. + p.Details = append(p.Details, &anypb.Any{ + TypeUrl: any.GetTypeUrl(), + Value: any.GetValue(), + }) + + // Any remaining details are wrapped errors. We check + // both versions of Unwrap to get this correct. + var errs []error + switch err := err.(type) { + case interface{ Unwrap() error }: + if unwrapped := err.Unwrap(); unwrapped != nil { + errs = []error{unwrapped} + } + case interface{ Unwrap() []error }: + errs = err.Unwrap() } - switch { + for _, err := range errs { + detail := &spb.Status{ + // Code doesn't matter. We don't use it beyond the top level. + // Set to unknown just in case it leaks somehow. + Code: int32(codes.Unknown), + Message: err.Error(), + } + withDetails(detail, err) + + if any, err := anypb.New(detail); err == nil { + p.Details = append(p.Details, any) + } + } +} + +func errorCode(err error) codes.Code { + switch err := errdefs.Resolve(err); { case errdefs.IsInvalidArgument(err): - return status.Error(codes.InvalidArgument, err.Error()) + return codes.InvalidArgument case errdefs.IsNotFound(err): - return status.Error(codes.NotFound, err.Error()) + return codes.NotFound case errdefs.IsAlreadyExists(err): - return status.Error(codes.AlreadyExists, err.Error()) - case errdefs.IsFailedPrecondition(err) || errdefs.IsConflict(err) || errdefs.IsNotModified(err): - return status.Error(codes.FailedPrecondition, err.Error()) + return codes.AlreadyExists + case errdefs.IsFailedPrecondition(err): + fallthrough + case errdefs.IsConflict(err): + fallthrough + case errdefs.IsNotModified(err): + return codes.FailedPrecondition case errdefs.IsUnavailable(err): - return status.Error(codes.Unavailable, err.Error()) + return codes.Unavailable case errdefs.IsNotImplemented(err): - return status.Error(codes.Unimplemented, err.Error()) + return codes.Unimplemented case errdefs.IsCanceled(err): - return status.Error(codes.Canceled, err.Error()) + return codes.Canceled case errdefs.IsDeadlineExceeded(err): - return status.Error(codes.DeadlineExceeded, err.Error()) + return codes.DeadlineExceeded case errdefs.IsUnauthorized(err): - return status.Error(codes.Unauthenticated, err.Error()) + return codes.Unauthenticated case errdefs.IsPermissionDenied(err): - return status.Error(codes.PermissionDenied, err.Error()) + return codes.PermissionDenied case errdefs.IsInternal(err): - return status.Error(codes.Internal, err.Error()) + return codes.Internal case errdefs.IsDataLoss(err): - return status.Error(codes.DataLoss, err.Error()) + return codes.DataLoss case errdefs.IsAborted(err): - return status.Error(codes.Aborted, err.Error()) + return codes.Aborted case errdefs.IsOutOfRange(err): - return status.Error(codes.OutOfRange, err.Error()) + return codes.OutOfRange case errdefs.IsResourceExhausted(err): - return status.Error(codes.ResourceExhausted, err.Error()) - case errdefs.IsUnknown(err): - return status.Error(codes.Unknown, err.Error()) + return codes.ResourceExhausted + default: + return codes.Unknown } - - return err } // ToGRPCf maps the error to grpc error codes, assembling the formatting string @@ -98,6 +161,95 @@ func ToGRPCf(err error, format string, args ...interface{}) error { return ToGRPC(fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)) } +func FromGRPC(err error) error { + if err == nil { + return nil + } + + st, ok := status.FromError(err) + if !ok { + return err + } + + p := st.Proto() + return fromGRPCProto(p) +} + +func fromGRPCProto(p *spb.Status) error { + err := errors.New(p.Message) + if len(p.Details) == 0 { + return err + } + + // First detail has the serialization. + detail := p.Details[0] + v, terr := typeurl.UnmarshalAny(detail) + if terr == nil { + if verr, ok := v.(error); ok { + // Successfully unmarshaled the type as an error. + // Use this instead. + err = verr + } + } + + // If there is more than one detail, attempt to unmarshal + // each one of them. + if len(p.Details) > 1 { + wrapped := make([]error, 0, len(p.Details)-1) + for _, detail := range p.Details[1:] { + p, derr := typeurl.UnmarshalAny(detail) + if derr != nil { + continue + } + + switch p := p.(type) { + case *spb.Status: + wrapped = append(wrapped, fromGRPCProto(p)) + } + } + + // If the error supports WrapError, then use that + // to modify the error to include the wrapped error. + // Otherwise, we create a proxy type so Unwrap works. + if wrapper, ok := err.(interface{ WrapError(err error) }); ok { + for _, w := range wrapped { + wrapper.WrapError(w) + } + } else { + if len(wrapped) == 1 { + err = &wrapError{ + error: err, + err: wrapped[0], + } + } else { + err = &wrapErrors{ + error: err, + errs: wrapped, + } + } + } + } + return err +} + +type wrapError struct { + error + err error +} + +func (e *wrapError) Unwrap() error { + return e.err +} + +type wrapErrors struct { + error + errs []error +} + +func (e *wrapErrors) Unwrap() []error { + return e.errs +} + // ToNative returns the underlying error from a grpc service based on the grpc error code func ToNative(err error) error { if err == nil { diff --git a/errgrpc/grpc_test.go b/errgrpc/grpc_test.go index 7a3778c..75615f5 100644 --- a/errgrpc/grpc_test.go +++ b/errgrpc/grpc_test.go @@ -172,3 +172,26 @@ func TestGRPCRoundTrip(t *testing.T) { }) } } + +func TestGRPC(t *testing.T) { + err := fmt.Errorf("my error: %w", errdefs.ErrAborted) + gerr := ToGRPC(err) + st, _ := status.FromError(gerr) + p := st.Proto() + fmt.Printf("%d\n", len(p.Details)) + ferr := FromGRPC(gerr) + + for unvisited := []error{ferr}; len(unvisited) > 0; { + cur := unvisited[0] + unvisited = unvisited[1:] + fmt.Printf("%s %T\n", cur.Error(), cur) + switch cur := cur.(type) { + case interface{ Unwrap() error }: + if v := cur.Unwrap(); v != nil { + unvisited = append(unvisited, v) + } + case interface{ Unwrap() []error }: + unvisited = append(unvisited, cur.Unwrap()...) + } + } +}