Skip to content

Commit b9dce4d

Browse files
committed
Add support for grpc error details
When multiple errors are given, use details to encode errors into the grpc status and decode details back into errors. Signed-off-by: Derek McGowan <[email protected]>
1 parent ffb0349 commit b9dce4d

3 files changed

Lines changed: 324 additions & 69 deletions

File tree

errgrpc/grpc.go

Lines changed: 218 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -23,71 +23,175 @@ package errgrpc
2323

2424
import (
2525
"context"
26+
"errors"
2627
"fmt"
28+
"reflect"
2729
"strconv"
2830
"strings"
2931

32+
spb "google.golang.org/genproto/googleapis/rpc/status"
3033
"google.golang.org/grpc/codes"
3134
"google.golang.org/grpc/status"
35+
"google.golang.org/protobuf/proto"
36+
"google.golang.org/protobuf/protoadapt"
37+
"google.golang.org/protobuf/types/known/anypb"
38+
39+
"github.com/containerd/typeurl/v2"
3240

3341
"github.com/containerd/errdefs"
3442
"github.com/containerd/errdefs/internal/cause"
43+
"github.com/containerd/errdefs/internal/types"
3544
)
3645

37-
// ToGRPC will attempt to map the backend containerd error into a grpc error,
38-
// using the original error message as a description.
46+
// ToGRPC will attempt to map the error into a grpc error, from the error types
47+
// defined in the the errdefs package and attempign to preserve the original
48+
// description. Any type which does not resolve to a defined error type will
49+
// be assigned the unknown error code.
3950
//
4051
// Further information may be extracted from certain errors depending on their
41-
// type.
52+
// type. The grpc error details will be used to attempt to preserve as much of
53+
// the error structures and types as possible.
54+
//
55+
// Errors which can be marshaled using protobuf or typeurl will be considered
56+
// for including as GRPC error details.
57+
// Additionally, use the following interfaces in errors to preserve custom types:
4258
//
43-
// If the error is unmapped, the original error will be returned to be handled
44-
// by the regular grpc error handling stack.
59+
// WrapError(error) error - Used to wrap the previous error
60+
// JoinErrors(...error) error - Used to join all previous errors
61+
// CollapseError() - Used for errors which carry information but
62+
// should not have their error message shown.
4563
func ToGRPC(err error) error {
4664
if err == nil {
4765
return nil
4866
}
4967

50-
if isGRPCError(err) {
68+
if _, ok := status.FromError(err); ok {
5169
// error has already been mapped to grpc
5270
return err
5371
}
72+
st := statusFromError(err)
73+
if st != nil {
74+
if details := errorDetails(err, false); len(details) > 0 {
75+
if ds, _ := st.WithDetails(details...); ds != nil {
76+
st = ds
77+
}
78+
}
79+
err = st.Err()
80+
}
81+
return err
82+
}
5483

55-
switch {
56-
case errdefs.IsInvalidArgument(err):
57-
return status.Error(codes.InvalidArgument, err.Error())
58-
case errdefs.IsNotFound(err):
59-
return status.Error(codes.NotFound, err.Error())
60-
case errdefs.IsAlreadyExists(err):
61-
return status.Error(codes.AlreadyExists, err.Error())
62-
case errdefs.IsFailedPrecondition(err) || errdefs.IsConflict(err) || errdefs.IsNotModified(err):
63-
return status.Error(codes.FailedPrecondition, err.Error())
64-
case errdefs.IsUnavailable(err):
65-
return status.Error(codes.Unavailable, err.Error())
66-
case errdefs.IsNotImplemented(err):
67-
return status.Error(codes.Unimplemented, err.Error())
68-
case errdefs.IsCanceled(err):
69-
return status.Error(codes.Canceled, err.Error())
70-
case errdefs.IsDeadlineExceeded(err):
71-
return status.Error(codes.DeadlineExceeded, err.Error())
72-
case errdefs.IsUnauthorized(err):
73-
return status.Error(codes.Unauthenticated, err.Error())
74-
case errdefs.IsPermissionDenied(err):
75-
return status.Error(codes.PermissionDenied, err.Error())
76-
case errdefs.IsInternal(err):
77-
return status.Error(codes.Internal, err.Error())
78-
case errdefs.IsDataLoss(err):
79-
return status.Error(codes.DataLoss, err.Error())
80-
case errdefs.IsAborted(err):
81-
return status.Error(codes.Aborted, err.Error())
82-
case errdefs.IsOutOfRange(err):
83-
return status.Error(codes.OutOfRange, err.Error())
84-
case errdefs.IsResourceExhausted(err):
85-
return status.Error(codes.ResourceExhausted, err.Error())
86-
case errdefs.IsUnknown(err):
87-
return status.Error(codes.Unknown, err.Error())
84+
func statusFromError(err error) *status.Status {
85+
switch errdefs.Resolve(err) {
86+
case errdefs.ErrInvalidArgument:
87+
return status.New(codes.InvalidArgument, err.Error())
88+
case errdefs.ErrNotFound:
89+
return status.New(codes.NotFound, err.Error())
90+
case errdefs.ErrAlreadyExists:
91+
return status.New(codes.AlreadyExists, err.Error())
92+
case errdefs.ErrPermissionDenied:
93+
return status.New(codes.PermissionDenied, err.Error())
94+
case errdefs.ErrResourceExhausted:
95+
return status.New(codes.ResourceExhausted, err.Error())
96+
case errdefs.ErrFailedPrecondition, errdefs.ErrConflict, errdefs.ErrNotModified:
97+
return status.New(codes.FailedPrecondition, err.Error())
98+
case errdefs.ErrAborted:
99+
return status.New(codes.Aborted, err.Error())
100+
case errdefs.ErrOutOfRange:
101+
return status.New(codes.OutOfRange, err.Error())
102+
case errdefs.ErrNotImplemented:
103+
return status.New(codes.Unimplemented, err.Error())
104+
case errdefs.ErrInternal:
105+
return status.New(codes.Internal, err.Error())
106+
case errdefs.ErrUnavailable:
107+
return status.New(codes.Unavailable, err.Error())
108+
case errdefs.ErrDataLoss:
109+
return status.New(codes.DataLoss, err.Error())
110+
case errdefs.ErrUnauthenticated:
111+
return status.New(codes.Unauthenticated, err.Error())
112+
case context.DeadlineExceeded:
113+
return status.New(codes.DeadlineExceeded, err.Error())
114+
case context.Canceled:
115+
return status.New(codes.Canceled, err.Error())
116+
case errdefs.ErrUnknown:
117+
return status.New(codes.Unknown, err.Error())
88118
}
119+
return nil
120+
}
89121

90-
return err
122+
// errorDetails returns an array of errors which make up the provided error.
123+
// If firstIncluded is true, then all encodable errors will be used, otherwise
124+
// the first error in an error list will be not be used, to account for the
125+
// the base status error which details are added to via wrap or join.
126+
//
127+
// The errors are ordered in way that they can be applied in order by either
128+
// wrapping or joining the errors to recreate an error with the same structure
129+
// when `WrapError` and `JoinErrors` interfaces are used.
130+
//
131+
// The intent is that when re-applying the errors to create a single error, the
132+
// results of calls to `Error()`, `errors.Is`, `errors.As`, and "%+v" formatting
133+
// is the same as the original error.
134+
func errorDetails(err error, firstIncluded bool) []protoadapt.MessageV1 {
135+
switch uerr := err.(type) {
136+
case interface{ Unwrap() error }:
137+
details := errorDetails(uerr.Unwrap(), firstIncluded)
138+
139+
// If the type is able to wrap, then include if proto
140+
if _, ok := err.(interface{ WrapError(error) error }); ok {
141+
// Get proto message
142+
if protoErr := toProtoMessage(err); protoErr != nil {
143+
details = append(details, protoErr)
144+
}
145+
}
146+
147+
return details
148+
case interface{ Unwrap() []error }:
149+
var details []protoadapt.MessageV1
150+
for i, e := range uerr.Unwrap() {
151+
details = append(details, errorDetails(e, firstIncluded || i > 0)...)
152+
}
153+
154+
if _, ok := err.(interface{ JoinErrors(...error) error }); ok {
155+
// Get proto message
156+
if protoErr := toProtoMessage(err); protoErr != nil {
157+
details = append(details, protoErr)
158+
}
159+
}
160+
return details
161+
}
162+
163+
if firstIncluded {
164+
if protoErr := toProtoMessage(err); protoErr != nil {
165+
return []protoadapt.MessageV1{protoErr}
166+
}
167+
if gs, ok := status.FromError(ToGRPC(err)); ok {
168+
return []protoadapt.MessageV1{gs.Proto()}
169+
}
170+
// TODO: Else include unknown extra error type?
171+
}
172+
173+
return nil
174+
}
175+
176+
func toProtoMessage(err error) protoadapt.MessageV1 {
177+
// Do not double encode proto messages, otherwise use Any
178+
if pm, ok := err.(protoadapt.MessageV1); ok {
179+
return pm
180+
}
181+
if pm, ok := err.(proto.Message); ok {
182+
return protoadapt.MessageV1Of(pm)
183+
}
184+
185+
if reflect.TypeOf(err).Kind() == reflect.Ptr {
186+
a, aerr := typeurl.MarshalAny(err)
187+
if aerr == nil {
188+
return &anypb.Any{
189+
TypeUrl: a.GetTypeUrl(),
190+
Value: a.GetValue(),
191+
}
192+
}
193+
}
194+
return nil
91195
}
92196

93197
// ToGRPCf maps the error to grpc error codes, assembling the formatting string
@@ -98,17 +202,32 @@ func ToGRPCf(err error, format string, args ...interface{}) error {
98202
return ToGRPC(fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err))
99203
}
100204

101-
// ToNative returns the underlying error from a grpc service based on the grpc error code
205+
// ToNative returns the underlying error from a grpc service based on the grpc
206+
// error code. The grpc details are used to add wrap the error in more context
207+
// or support multiple errors.
102208
func ToNative(err error) error {
103209
if err == nil {
104210
return nil
105211
}
106212

107-
desc := errDesc(err)
213+
s, isGRPC := status.FromError(err)
214+
215+
var (
216+
desc string
217+
code codes.Code
218+
)
219+
220+
if isGRPC {
221+
desc = s.Message()
222+
code = s.Code()
223+
} else {
224+
desc = err.Error()
225+
code = codes.Unknown
226+
}
108227

109228
var cls error // divide these into error classes, becomes the cause
110229

111-
switch code(err) {
230+
switch code {
112231
case codes.InvalidArgument:
113232
cls = errdefs.ErrInvalidArgument
114233
case codes.AlreadyExists:
@@ -118,6 +237,10 @@ func ToNative(err error) error {
118237
case codes.Unavailable:
119238
cls = errdefs.ErrUnavailable
120239
case codes.FailedPrecondition:
240+
// TODO: Has suffix is not sufficient for conflict and not modified
241+
// Message should start with ": " or be at beginning of a line
242+
// Message should end with ": " or be at the end of a line
243+
// Compile a regex
121244
if desc == errdefs.ErrConflict.Error() || strings.HasSuffix(desc, ": "+errdefs.ErrConflict.Error()) {
122245
cls = errdefs.ErrConflict
123246
} else if desc == errdefs.ErrNotModified.Error() || strings.HasSuffix(desc, ": "+errdefs.ErrNotModified.Error()) {
@@ -147,7 +270,7 @@ func ToNative(err error) error {
147270
cls = errdefs.ErrResourceExhausted
148271
default:
149272
if idx := strings.LastIndex(desc, cause.UnexpectedStatusPrefix); idx > 0 {
150-
if status, err := strconv.Atoi(desc[idx+len(cause.UnexpectedStatusPrefix):]); err == nil && status >= 200 && status < 600 {
273+
if status, uerr := strconv.Atoi(desc[idx+len(cause.UnexpectedStatusPrefix):]); uerr == nil && status >= 200 && status < 600 {
151274
cls = cause.ErrUnexpectedStatus{Status: status}
152275
}
153276
}
@@ -157,10 +280,59 @@ func ToNative(err error) error {
157280
}
158281

159282
msg := rebaseMessage(cls, desc)
160-
if msg != "" {
283+
if msg == "" {
284+
err = cls
285+
} else if msg != desc {
161286
err = fmt.Errorf("%s: %w", msg, cls)
287+
} else if wm, ok := cls.(interface{ WithMessage(string) error }); ok {
288+
err = wm.WithMessage(msg)
162289
} else {
163-
err = cls
290+
err = fmt.Errorf("%s: %w", msg, cls)
291+
}
292+
293+
if isGRPC {
294+
errs := []error{err}
295+
for _, a := range s.Details() {
296+
var derr error
297+
298+
// First decode error if needed
299+
if s, ok := a.(*spb.Status); ok {
300+
derr = ToNative(status.ErrorProto(s))
301+
} else if e, ok := a.(error); ok {
302+
derr = e
303+
} else if dany, ok := a.(typeurl.Any); ok {
304+
i, uerr := typeurl.UnmarshalAny(dany)
305+
if uerr == nil {
306+
if e, ok = i.(error); ok {
307+
derr = e
308+
} else {
309+
derr = fmt.Errorf("non-error unmarshalled detail: %v", i)
310+
}
311+
} else {
312+
derr = fmt.Errorf("error of type %q with failure to unmarshal: %v", dany.GetTypeUrl(), uerr)
313+
}
314+
} else {
315+
derr = fmt.Errorf("non-error detail: %v", a)
316+
}
317+
318+
switch werr := derr.(type) {
319+
case interface{ WrapError(error) error }:
320+
errs[len(errs)-1] = werr.WrapError(errs[len(errs)-1])
321+
case interface{ JoinErrors(...error) error }:
322+
// TODO: Consider whether this should support joining a subset
323+
errs[0] = werr.JoinErrors(errs...)
324+
case interface{ CollapseError() }:
325+
errs[len(errs)-1] = types.CollapsedError(errs[len(errs)-1], derr)
326+
default:
327+
errs = append(errs, derr)
328+
}
329+
330+
}
331+
if len(errs) > 1 {
332+
err = errors.Join(errs...)
333+
} else {
334+
err = errs[0]
335+
}
164336
}
165337

166338
return err
@@ -179,22 +351,3 @@ func rebaseMessage(cls error, desc string) string {
179351

180352
return strings.TrimSuffix(desc, ": "+clss)
181353
}
182-
183-
func isGRPCError(err error) bool {
184-
_, ok := status.FromError(err)
185-
return ok
186-
}
187-
188-
func code(err error) codes.Code {
189-
if s, ok := status.FromError(err); ok {
190-
return s.Code()
191-
}
192-
return codes.Unknown
193-
}
194-
195-
func errDesc(err error) string {
196-
if s, ok := status.FromError(err); ok {
197-
return s.Message()
198-
}
199-
return err.Error()
200-
}

0 commit comments

Comments
 (0)