gRPC: support wrap/unwrap of berrors with suberrors. (#4278)
If a berror with suberrors is being wrapped then we must marshal the suberrors as JSON and include this data in the RPC metadata trailer that also carries the berror type. When unwrapping metadata with JSON suberrors they should be unmarshalled into the returned berror's suberrors.
This commit is contained in:
parent
0a16b5f57d
commit
5a1c18dd9f
|
@ -2,6 +2,7 @@ package grpc
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
|
@ -27,10 +28,29 @@ func wrapError(ctx context.Context, err error) error {
|
|||
return nil
|
||||
}
|
||||
if berr, ok := err.(*berrors.BoulderError); ok {
|
||||
pairs := []string{
|
||||
"errortype", strconv.Itoa(int(berr.Type)),
|
||||
}
|
||||
|
||||
// If there are suberrors then extend the metadata pairs to include the JSON
|
||||
// marshaling of the suberrors. Errors in marshaling are not ignored and
|
||||
// instead result in a return of an explicit InternalServerError and not
|
||||
// a wrapped error missing suberrors.
|
||||
if len(berr.SubErrors) > 0 {
|
||||
jsonSubErrs, err := json.Marshal(berr.SubErrors)
|
||||
if err != nil {
|
||||
return berrors.InternalServerError(
|
||||
"error marshaling json SubErrors, orig error %q",
|
||||
err)
|
||||
}
|
||||
pairs = append(pairs, "suberrors")
|
||||
pairs = append(pairs, string(jsonSubErrs))
|
||||
}
|
||||
|
||||
// Ignoring the error return here is safe because if setting the metadata
|
||||
// fails, we'll still return an error, but it will be interpreted on the
|
||||
// other side as an InternalServerError instead of a more specific one.
|
||||
_ = grpc.SetTrailer(ctx, metadata.Pairs("errortype", strconv.Itoa(int(berr.Type))))
|
||||
_ = grpc.SetTrailer(ctx, metadata.Pairs(pairs...))
|
||||
return grpc.Errorf(codes.Unknown, err.Error())
|
||||
}
|
||||
return grpc.Errorf(codes.Unknown, err.Error())
|
||||
|
@ -60,7 +80,25 @@ func unwrapError(err error, md metadata.MD) error {
|
|||
unwrappedErr,
|
||||
)
|
||||
}
|
||||
return berrors.New(berrors.ErrorType(errType), unwrappedErr)
|
||||
outErr := berrors.New(berrors.ErrorType(errType), unwrappedErr)
|
||||
if subErrsJSON, ok := md["suberrors"]; ok {
|
||||
if len(subErrsJSON) != 1 {
|
||||
return berrors.InternalServerError(
|
||||
"multiple suberrors metadata, wrapped error %q",
|
||||
unwrappedErr,
|
||||
)
|
||||
}
|
||||
var suberrs []berrors.SubBoulderError
|
||||
if err := json.Unmarshal([]byte(subErrsJSON[0]), &suberrs); err != nil {
|
||||
return berrors.InternalServerError(
|
||||
"error unmarshaling suberrs JSON %q, wrapped error %q",
|
||||
subErrsJSON[0],
|
||||
unwrappedErr,
|
||||
)
|
||||
}
|
||||
outErr = (outErr.(*berrors.BoulderError)).WithSubErrors(suberrs)
|
||||
}
|
||||
return outErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/jmhodges/clock"
|
||||
berrors "github.com/letsencrypt/boulder/errors"
|
||||
testproto "github.com/letsencrypt/boulder/grpc/test_proto"
|
||||
"github.com/letsencrypt/boulder/identifier"
|
||||
"github.com/letsencrypt/boulder/metrics"
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
)
|
||||
|
@ -52,3 +53,45 @@ func TestErrorWrapping(t *testing.T) {
|
|||
test.AssertEquals(t, wrapError(nil, nil), nil)
|
||||
test.AssertEquals(t, unwrapError(nil, nil), nil)
|
||||
}
|
||||
|
||||
// TestSubErrorWrapping tests that a boulder error with suberrors can be
|
||||
// correctly wrapped and unwrapped across the RPC layer.
|
||||
func TestSubErrorWrapping(t *testing.T) {
|
||||
serverMetrics := NewServerMetrics(metrics.NewNoopScope())
|
||||
si := newServerInterceptor(serverMetrics, clock.NewFake())
|
||||
ci := clientInterceptor{time.Second, NewClientMetrics(metrics.NewNoopScope()), clock.NewFake()}
|
||||
srv := grpc.NewServer(grpc.UnaryInterceptor(si.intercept))
|
||||
es := &errorServer{}
|
||||
testproto.RegisterChillerServer(srv, es)
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:")
|
||||
test.AssertNotError(t, err, "Failed to create listener")
|
||||
go func() { _ = srv.Serve(lis) }()
|
||||
defer srv.Stop()
|
||||
|
||||
conn, err := grpc.Dial(
|
||||
lis.Addr().String(),
|
||||
grpc.WithInsecure(),
|
||||
grpc.WithUnaryInterceptor(ci.intercept),
|
||||
)
|
||||
test.AssertNotError(t, err, "Failed to dial grpc test server")
|
||||
client := testproto.NewChillerClient(conn)
|
||||
|
||||
subErrors := []berrors.SubBoulderError{
|
||||
{
|
||||
Identifier: identifier.DNSIdentifier("chillserver.com"),
|
||||
BoulderError: &berrors.BoulderError{
|
||||
Type: berrors.RejectedIdentifier,
|
||||
Detail: "2 ill 2 chill",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
es.err = (&berrors.BoulderError{
|
||||
Type: berrors.Malformed,
|
||||
Detail: "malformed chill req",
|
||||
}).WithSubErrors(subErrors)
|
||||
|
||||
_, err = client.Chill(context.Background(), &testproto.Time{})
|
||||
test.Assert(t, err != nil, fmt.Sprintf("nil error returned, expected: %s", err))
|
||||
test.AssertDeepEquals(t, err, es.err)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue