-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
errdefs: add support for typed errors #1454
Changes from 1 commit
02fff48
3f77f04
e4cc086
ae3b75d
cce301b
abbda4e
4b2636a
c511cdb
2fc9aee
725f5e1
90288ab
c78e875
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
package grpcerrors | ||
|
||
import ( | ||
"github.com/golang/protobuf/proto" | ||
"github.com/golang/protobuf/ptypes" | ||
"github.com/moby/buildkit/util/stack" | ||
spb "google.golang.org/genproto/googleapis/rpc/status" | ||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/status" | ||
) | ||
|
||
type TypedError interface { | ||
ToProto() TypedErrorProto | ||
} | ||
|
||
type TypedErrorProto interface { | ||
proto.Message | ||
WrapError(error) error | ||
} | ||
|
||
func ToGRPC(err error) error { | ||
if err == nil { | ||
return nil | ||
} | ||
st, ok := AsGRPCStatus(err) | ||
if !ok || st == nil { | ||
st = status.New(Code(err), err.Error()) | ||
} | ||
if st.Code() != Code(err) { | ||
pb := st.Proto() | ||
pb.Code = int32(Code(err)) | ||
st = status.FromProto(pb) | ||
} | ||
|
||
var details []proto.Message | ||
|
||
for _, st := range stack.Traces(err) { | ||
details = append(details, st) | ||
} | ||
|
||
each(err, func(err error) { | ||
if te, ok := err.(TypedError); ok { | ||
details = append(details, te.ToProto()) | ||
} | ||
}) | ||
|
||
if len(details) > 0 { | ||
if st2, err := st.WithDetails(details...); err == nil { | ||
st = st2 | ||
} | ||
} | ||
|
||
return st.Err() | ||
} | ||
|
||
func Code(err error) codes.Code { | ||
if se, ok := err.(interface { | ||
Code() codes.Code | ||
}); ok { | ||
return se.Code() | ||
} | ||
|
||
wrapped, ok := err.(interface { | ||
Unwrap() error | ||
}) | ||
if ok { | ||
return Code(wrapped.Unwrap()) | ||
} | ||
|
||
return status.FromContextError(err).Code() | ||
} | ||
|
||
func WrapCode(err error, code codes.Code) error { | ||
return &withCode{error: err, code: code} | ||
} | ||
|
||
func AsGRPCStatus(err error) (*status.Status, bool) { | ||
if err == nil { | ||
return nil, true | ||
} | ||
if se, ok := err.(interface { | ||
GRPCStatus() *status.Status | ||
}); ok { | ||
return se.GRPCStatus(), true | ||
} | ||
|
||
wrapped, ok := err.(interface { | ||
Unwrap() error | ||
}) | ||
if ok { | ||
return AsGRPCStatus(wrapped.Unwrap()) | ||
} | ||
|
||
return nil, false | ||
} | ||
|
||
func FromGRPC(err error) error { | ||
if err == nil { | ||
return nil | ||
} | ||
st, ok := status.FromError(err) | ||
if !ok { | ||
return err | ||
} | ||
|
||
pb := st.Proto() | ||
|
||
n := &spb.Status{ | ||
Code: pb.Code, | ||
Message: pb.Message, | ||
} | ||
|
||
details := make([]TypedErrorProto, 0, len(pb.Details)) | ||
stacks := make([]*stack.Stack, 0, len(pb.Details)) | ||
|
||
// details that we don't understand are copied as proto | ||
for _, d := range pb.Details { | ||
detail := &ptypes.DynamicAny{} | ||
if err := ptypes.UnmarshalAny(d, detail); err != nil { | ||
n.Details = append(n.Details, d) | ||
continue | ||
} | ||
|
||
switch v := detail.Message.(type) { | ||
case *stack.Stack: | ||
stacks = append(stacks, v) | ||
case TypedErrorProto: | ||
details = append(details, v) | ||
default: | ||
n.Details = append(n.Details, d) | ||
} | ||
|
||
} | ||
|
||
err = status.FromProto(n).Err() | ||
|
||
for _, s := range stacks { | ||
if s != nil { | ||
err = stack.Wrap(err, *s) | ||
} | ||
} | ||
|
||
for _, d := range details { | ||
err = d.WrapError(err) | ||
} | ||
|
||
return stack.Enable(err) | ||
} | ||
|
||
type withCode struct { | ||
code codes.Code | ||
error | ||
} | ||
|
||
func (e *withCode) Unwrap() error { | ||
return e.error | ||
} | ||
|
||
func each(err error, fn func(error)) { | ||
fn(err) | ||
if wrapped, ok := err.(interface { | ||
Unwrap() error | ||
}); ok { | ||
each(wrapped.Unwrap(), fn) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package grpcerrors | ||
|
||
import ( | ||
"context" | ||
|
||
"google.golang.org/grpc" | ||
) | ||
|
||
func UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { | ||
resp, err = handler(ctx, req) | ||
if err != nil { | ||
err = ToGRPC(err) | ||
} | ||
return resp, err | ||
} | ||
|
||
func StreamServerInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { | ||
return ToGRPC(handler(srv, ss)) | ||
} | ||
|
||
func UnaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { | ||
return FromGRPC(invoker(ctx, method, req, reply, cc, opts...)) | ||
} | ||
|
||
func StreamClientInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { | ||
s, err := streamer(ctx, desc, cc, method, opts...) | ||
return s, ToGRPC(err) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package stack | ||
|
||
//go:generate protoc -I=. -I=../../vendor/ --go_out=. stack.proto |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
package stack | ||
|
||
import ( | ||
"fmt" | ||
io "io" | ||
"os" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/pkg/errors" | ||
) | ||
|
||
var version string | ||
var revision string | ||
|
||
func SetVersionInfo(v, r string) { | ||
version = v | ||
revision = r | ||
} | ||
|
||
func Traces(err error) []*Stack { | ||
var st []*Stack | ||
|
||
wrapped, ok := err.(interface { | ||
Unwrap() error | ||
}) | ||
if ok { | ||
st = Traces(wrapped.Unwrap()) | ||
} | ||
|
||
if ste, ok := err.(interface { | ||
StackTrace() errors.StackTrace | ||
}); ok { | ||
st = append(st, convertStack(ste.StackTrace())) | ||
} | ||
|
||
if ste, ok := err.(interface { | ||
StackTrace() *Stack | ||
}); ok { | ||
st = append(st, ste.StackTrace()) | ||
} | ||
|
||
return st | ||
} | ||
|
||
func Enable(err error) error { | ||
if err == nil { | ||
return nil | ||
} | ||
if !hasLocalStackTrace(err) { | ||
return errors.WithStack(err) | ||
} | ||
return err | ||
} | ||
|
||
func Wrap(err error, s Stack) error { | ||
return &withStack{stack: s, error: err} | ||
} | ||
|
||
func hasLocalStackTrace(err error) bool { | ||
wrapped, ok := err.(interface { | ||
Unwrap() error | ||
}) | ||
if ok && hasLocalStackTrace(wrapped.Unwrap()) { | ||
return true | ||
} | ||
|
||
_, ok = err.(interface { | ||
StackTrace() errors.StackTrace | ||
}) | ||
return ok | ||
} | ||
|
||
func StackFormatter(err error) fmt.Formatter { | ||
return &stackFormatter{err} | ||
} | ||
|
||
type stackFormatter struct { | ||
error | ||
} | ||
|
||
func (w *stackFormatter) Format(s fmt.State, verb rune) { | ||
switch verb { | ||
case 'v': | ||
if s.Flag('+') { | ||
fmt.Fprintf(s, "%+v\n", w.Error()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. w.Error() always returns a string, so no need for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this basically should be |
||
for _, stack := range Traces(w.error) { | ||
fmt.Fprintf(s, "%d %s %s\n", stack.Pid, stack.Version, strings.Join(stack.Cmdline, " ")) | ||
for _, f := range stack.Frames { | ||
fmt.Fprintf(s, "%s\n\t%s:%d\n", f.Name, f.File, f.Line) | ||
} | ||
fmt.Fprintf(s, "\n") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fprintln |
||
} | ||
return | ||
} | ||
fallthrough | ||
case 's': | ||
io.WriteString(s, w.Error()) | ||
case 'q': | ||
fmt.Fprintf(s, "%q", w.Error()) | ||
} | ||
} | ||
|
||
func convertStack(s errors.StackTrace) *Stack { | ||
var out Stack | ||
for _, f := range s { | ||
dt, err := f.MarshalText() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤮 |
||
if err != nil { | ||
continue | ||
} | ||
p := strings.SplitN(string(dt), " ", 2) | ||
if len(p) != 2 { | ||
continue | ||
} | ||
idx := strings.LastIndexByte(p[1], ':') | ||
if idx == -1 { | ||
continue | ||
} | ||
line, err := strconv.Atoi(p[1][idx+1:]) | ||
if err != nil { | ||
continue | ||
} | ||
out.Frames = append(out.Frames, &Frame{ | ||
Name: p[0], | ||
File: p[1][:idx], | ||
Line: int32(line), | ||
}) | ||
} | ||
out.Cmdline = os.Args | ||
out.Pid = int32(os.Getpid()) | ||
out.Version = version | ||
out.Revision = revision | ||
return &out | ||
} | ||
|
||
type withStack struct { | ||
stack Stack | ||
error | ||
} | ||
|
||
func (e *withStack) Unwrap() error { | ||
return e.error | ||
} | ||
|
||
func (e *withStack) StackTrace() *Stack { | ||
return &e.stack | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when would you do FromGRPC(notAGRPCError{})? Maybe do (error, bool) like status.FromError.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this doesn't return any extra info/type so I don't think bool is needed and just complicated the caller side