Skip to content
This repository has been archived by the owner on Sep 1, 2023. It is now read-only.

Commit

Permalink
Replace *anypb.Any with interface in error details
Browse files Browse the repository at this point in the history
Enough people rely on gRPC's error details API that we have to support
it, which requires some way of working with the Any WKT. However, we
don't like Any and plan to eventually introduce a better wrapper type,
so we don't want to couple our APIs directly to the concrete *anypb.Any
type.

This PR replaces our use of *anypb.Any with an interface that captures
the essential bits of Any's behavior. Even if we replace Any, our new
type can easily (and probably should) implement this interface.
  • Loading branch information
akshayjshah committed Feb 28, 2022
1 parent 735d799 commit aa66550
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 47 deletions.
6 changes: 4 additions & 2 deletions client_duplex_stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,13 +323,15 @@ func extractError(protobuf codec.Codec, h http.Header) *Error {
if len(detailsBinaryEncoded) > 0 {
detailsBinary, err := DecodeBinaryHeader(detailsBinaryEncoded)
if err != nil {
return Errorf(CodeUnknown, "server returned invalid grpc-error-details-bin trailer: %w", err)
return Errorf(CodeUnknown, "server returned invalid grpc-status-details-bin trailer: %w", err)
}
var status statuspb.Status
if err := protobuf.Unmarshal(detailsBinary, &status); err != nil {
return Errorf(CodeUnknown, "server returned invalid protobuf for error details: %w", err)
}
ret.details = status.Details
for _, d := range status.Details {
ret.details = append(ret.details, d)
}
// Prefer the protobuf-encoded data to the headers (grpc-go does this too).
ret.code = Code(status.Code)
ret.err = errors.New(status.Message)
Expand Down
59 changes: 24 additions & 35 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,25 @@ import (
"fmt"

"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/reflect/protoreflect"
)

// An ErrorDetail is a protobuf message attached to an *Error. Error details
// are sent over the network to clients, which can then work with
// strongly-typed data rather than trying to parse a complex error message.
//
// The ErrorDetail interface is implemented by protobuf's Any type, provided in
// Go by the google.golang.org/protobuf/types/known/anypb package. The
// google.golang.org/genproto/googleapis/rpc/errdetails package contains a
// variety of protobuf messages commonly used as error details.
type ErrorDetail interface {
proto.Message

MessageIs(proto.Message) bool
MessageName() protoreflect.FullName
UnmarshalTo(proto.Message) error
}

// An Error captures three pieces of information: a Code, a human-readable
// message, and an optional collection of arbitrary protobuf messages called
// "details" (more on those below). Servers send the code, message, and details
Expand All @@ -34,7 +50,7 @@ import (
type Error struct {
code Code
err error
details []*anypb.Any
details []ErrorDetail
}

// Wrap annotates any error with a status code. If the code is CodeOK, the
Expand All @@ -60,7 +76,7 @@ func AsError(err error) (*Error, bool) {
}

func (e *Error) Error() string {
text := fmt.Sprintf("%v", e.err)
text := e.err.Error()
if text == "" {
return e.code.String()
}
Expand All @@ -81,41 +97,14 @@ func (e *Error) Code() Code {
return e.code
}

// Details returns a deep copy of the error's details.
func (e *Error) Details() []*anypb.Any {
if len(e.details) == 0 {
return nil
}
ds := make([]*anypb.Any, len(e.details))
for i, d := range e.details {
ds[i] = proto.Clone(d).(*anypb.Any)
}
return ds
// Details returns the error's details.
func (e *Error) Details() []ErrorDetail {
return e.details
}

// AddDetail appends a message to the error's details.
func (e *Error) AddDetail(m proto.Message) error {
if d, ok := m.(*anypb.Any); ok {
e.details = append(e.details, proto.Clone(d).(*anypb.Any))
return nil
}
detail, err := anypb.New(m)
if err != nil {
return fmt.Errorf("can't add message to error details: %w", err)
}
e.details = append(e.details, detail)
return nil
}

// SetDetails overwrites the error's details.
func (e *Error) SetDetails(details ...proto.Message) error {
e.details = make([]*anypb.Any, 0, len(details))
for _, d := range details {
if err := e.AddDetail(d); err != nil {
return err
}
}
return nil
func (e *Error) AddDetail(d ErrorDetail) {
e.details = append(e.details, d)
}

// CodeOf returns the error's status code if it is or wraps a *rerpc.Error,
Expand Down
6 changes: 2 additions & 4 deletions error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ func TestErrorDetails(t *testing.T) {
assert.Nil(t, err, "create anypb.Any")
rerr := Errorf(CodeUnknown, "details")
assert.Zero(t, rerr.Details(), "fresh error")
assert.Nil(t, rerr.AddDetail(second), "add detail")
assert.Equal(t, rerr.Details(), []*anypb.Any{detail}, "retrieve details")
assert.Nil(t, rerr.SetDetails(second, second), "overwrite details")
assert.Equal(t, rerr.Details(), []*anypb.Any{detail, detail}, "retrieve details")
rerr.AddDetail(detail)
assert.Equal(t, rerr.Details(), []ErrorDetail{detail}, "retrieve details")
}
30 changes: 26 additions & 4 deletions handler_stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strconv"

"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"

"github.com/rerpc/rerpc/codec"
"github.com/rerpc/rerpc/compress"
Expand Down Expand Up @@ -91,7 +92,10 @@ func (hs *handlerSender) sendErrorGRPC(err error) error {
hs.writer.Header().Set("Grpc-Status-Details-Bin", "")
return nil
}
s := statusFromError(err)
s, statusError := statusFromError(err)
if statusError != nil {
return statusError
}
code := strconv.Itoa(int(s.Code))
bin, err := hs.protobuf.Marshal(s)
if err != nil {
Expand Down Expand Up @@ -151,19 +155,37 @@ func (hr *handlerReceiver) Header() http.Header {
return hr.request.Header
}

func statusFromError(err error) *statuspb.Status {
func statusFromError(err error) (*statuspb.Status, *Error) {
s := &statuspb.Status{
Code: int32(CodeUnknown),
Message: err.Error(),
}
if re, ok := AsError(err); ok {
s.Code = int32(re.Code())
s.Details = re.Details()
for _, d := range re.details {
// If the detail is already a protobuf Any, we're golden.
if anyProtoDetail, ok := d.(*anypb.Any); ok {
s.Details = append(s.Details, anyProtoDetail)
continue
}
// Otherwise, we convert it to an Any.
// TODO: Should we also attempt to delegate this to the detail by
// attempting an upcast to interface{ ToAny() *anypb.Any }?
anyProtoDetail, err := anypb.New(d)
if err != nil {
return nil, Errorf(
CodeInternal,
"can't create an *anypb.Any from %v (type %T): %v",
d, d, err,
)
}
s.Details = append(s.Details, anyProtoDetail)
}
if e := re.Unwrap(); e != nil {
s.Message = e.Error() // don't repeat code
}
}
return s
return s, nil
}

func discard(r io.Reader) {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit aa66550

Please sign in to comment.