155 lines
		
	
	
		
			4.0 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			155 lines
		
	
	
		
			4.0 KiB
		
	
	
	
		
			Go
		
	
	
	
| package grpc
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"strconv"
 | |
| 	"time"
 | |
| 
 | |
| 	"google.golang.org/grpc"
 | |
| 	"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, appErr error) error {
 | |
| 	if appErr == nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	var berr *berrors.BoulderError
 | |
| 	if errors.As(appErr, &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)
 | |
| 			}
 | |
| 			headerSafeSubErrs := strconv.QuoteToASCII(string(jsonSubErrs))
 | |
| 			pairs = append(pairs, "suberrors", headerSafeSubErrs)
 | |
| 		}
 | |
| 
 | |
| 		// 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())
 | |
| 		}
 | |
| 
 | |
| 		err := grpc.SetTrailer(ctx, metadata.Pairs(pairs...))
 | |
| 		if err != nil {
 | |
| 			return berrors.InternalServerError(
 | |
| 				"error setting gRPC error metadata, orig error %q", appErr)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return appErr
 | |
| }
 | |
| 
 | |
| // 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) //nolint:govet
 | |
| 	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,
 | |
| 			)
 | |
| 		}
 | |
| 
 | |
| 		unquotedSubErrors, unquoteErr := strconv.Unquote(subErrorsVal[0])
 | |
| 		if unquoteErr != nil {
 | |
| 			return fmt.Errorf(
 | |
| 				"unquoting 'suberrors' %q, wrapped error %q: %w",
 | |
| 				subErrorsVal[0],
 | |
| 				inErrMsg,
 | |
| 				unquoteErr,
 | |
| 			)
 | |
| 		}
 | |
| 
 | |
| 		unmarshalErr := json.Unmarshal([]byte(unquotedSubErrors), &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
 | |
| }
 |