boulder/grpc/errors.go

144 lines
3.9 KiB
Go

package grpc
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
berrors "github.com/letsencrypt/boulder/errors"
)
// wrapError wraps the internal error types we use for transport across the gRPC
// layer and appends an appropriate errortype to the gRPC trailer via the provided
// context. errors.BoulderError error types are encoded using the grpc/metadata
// in the context.Context for the RPC which is considered to be the 'proper'
// method of encoding custom error types (grpc/grpc#4543 and grpc/grpc-go#478)
func wrapError(ctx context.Context, err error) error {
if err == nil {
return nil
}
var berr *berrors.BoulderError
if errors.As(err, &berr) {
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", string(jsonSubErrs))
}
// If there is a RetryAfter value then extend the metadata pairs to
// include the value.
if berr.RetryAfter != 0 {
pairs = append(pairs, "retryafter", berr.RetryAfter.String())
}
// 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(pairs...))
return status.Errorf(codes.Unknown, err.Error())
}
return status.Errorf(codes.Unknown, err.Error())
}
// unwrapError unwraps errors returned from gRPC client calls which were wrapped
// with wrapError to their proper internal error type. If the provided metadata
// object has an "errortype" field, that will be used to set the type of the
// error.
func unwrapError(err error, md metadata.MD) error {
if err == nil {
return nil
}
errTypeStrs, ok := md["errortype"]
if !ok {
return err
}
inErrMsg := status.Convert(err).Message()
if len(errTypeStrs) != 1 {
return berrors.InternalServerError(
"multiple 'errortype' metadata, wrapped error %q",
inErrMsg,
)
}
inErrType, decErr := strconv.Atoi(errTypeStrs[0])
if decErr != nil {
return berrors.InternalServerError(
"failed to decode error type, decoding error %q, wrapped error %q",
decErr,
inErrMsg,
)
}
inErr := berrors.New(berrors.ErrorType(inErrType), inErrMsg)
var outErr *berrors.BoulderError
if !errors.As(inErr, &outErr) {
return fmt.Errorf(
"expected type of inErr to be %T got %T: %q",
outErr,
inErr,
inErr.Error(),
)
}
subErrorsVal, ok := md["suberrors"]
if ok {
if len(subErrorsVal) != 1 {
return berrors.InternalServerError(
"multiple 'suberrors' in metadata, wrapped error %q",
inErrMsg,
)
}
unmarshalErr := json.Unmarshal([]byte(subErrorsVal[0]), &outErr.SubErrors)
if unmarshalErr != nil {
return berrors.InternalServerError(
"JSON unmarshaling 'suberrors' %q, wrapped error %q: %s",
subErrorsVal[0],
inErrMsg,
unmarshalErr,
)
}
}
retryAfterVal, ok := md["retryafter"]
if ok {
if len(retryAfterVal) != 1 {
return berrors.InternalServerError(
"multiple 'retryafter' in metadata, wrapped error %q",
inErrMsg,
)
}
var parseErr error
outErr.RetryAfter, parseErr = time.ParseDuration(retryAfterVal[0])
if parseErr != nil {
return berrors.InternalServerError(
"parsing 'retryafter' as int64, wrapped error %q, parsing error: %s",
inErrMsg,
parseErr,
)
}
}
return outErr
}