github.com/letsencrypt/boulder@v0.20251208.0/grpc/errors.go (about)

     1  package grpc
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"strconv"
     9  	"time"
    10  
    11  	"google.golang.org/grpc"
    12  	"google.golang.org/grpc/metadata"
    13  	"google.golang.org/grpc/status"
    14  
    15  	berrors "github.com/letsencrypt/boulder/errors"
    16  )
    17  
    18  // wrapError wraps the internal error types we use for transport across the gRPC
    19  // layer and appends an appropriate errortype to the gRPC trailer via the provided
    20  // context. errors.BoulderError error types are encoded using the grpc/metadata
    21  // in the context.Context for the RPC which is considered to be the 'proper'
    22  // method of encoding custom error types (grpc/grpc#4543 and grpc/grpc-go#478)
    23  func wrapError(ctx context.Context, appErr error) error {
    24  	if appErr == nil {
    25  		return nil
    26  	}
    27  
    28  	var berr *berrors.BoulderError
    29  	if errors.As(appErr, &berr) {
    30  		pairs := []string{
    31  			"errortype", strconv.Itoa(int(berr.Type)),
    32  		}
    33  
    34  		// If there are suberrors then extend the metadata pairs to include the JSON
    35  		// marshaling of the suberrors. Errors in marshaling are not ignored and
    36  		// instead result in a return of an explicit InternalServerError and not
    37  		// a wrapped error missing suberrors.
    38  		if len(berr.SubErrors) > 0 {
    39  			jsonSubErrs, err := json.Marshal(berr.SubErrors)
    40  			if err != nil {
    41  				return berrors.InternalServerError(
    42  					"error marshaling json SubErrors, orig error %q", err)
    43  			}
    44  			headerSafeSubErrs := strconv.QuoteToASCII(string(jsonSubErrs))
    45  			pairs = append(pairs, "suberrors", headerSafeSubErrs)
    46  		}
    47  
    48  		// If there is a RetryAfter value then extend the metadata pairs to
    49  		// include the value.
    50  		if berr.RetryAfter != 0 {
    51  			pairs = append(pairs, "retryafter", berr.RetryAfter.String())
    52  		}
    53  
    54  		err := grpc.SetTrailer(ctx, metadata.Pairs(pairs...))
    55  		if err != nil {
    56  			return berrors.InternalServerError(
    57  				"error setting gRPC error metadata, orig error %q", appErr)
    58  		}
    59  	}
    60  
    61  	return appErr
    62  }
    63  
    64  // unwrapError unwraps errors returned from gRPC client calls which were wrapped
    65  // with wrapError to their proper internal error type. If the provided metadata
    66  // object has an "errortype" field, that will be used to set the type of the
    67  // error.
    68  func unwrapError(err error, md metadata.MD) error {
    69  	if err == nil {
    70  		return nil
    71  	}
    72  
    73  	errTypeStrs, ok := md["errortype"]
    74  	if !ok {
    75  		return err
    76  	}
    77  
    78  	inErrMsg := status.Convert(err).Message()
    79  	if len(errTypeStrs) != 1 {
    80  		return berrors.InternalServerError(
    81  			"multiple 'errortype' metadata, wrapped error %q",
    82  			inErrMsg,
    83  		)
    84  	}
    85  
    86  	inErrType, decErr := strconv.Atoi(errTypeStrs[0])
    87  	if decErr != nil {
    88  		return berrors.InternalServerError(
    89  			"failed to decode error type, decoding error %q, wrapped error %q",
    90  			decErr,
    91  			inErrMsg,
    92  		)
    93  	}
    94  	inErr := berrors.New(berrors.ErrorType(inErrType), inErrMsg)
    95  	var outErr *berrors.BoulderError
    96  	if !errors.As(inErr, &outErr) {
    97  		return fmt.Errorf(
    98  			"expected type of inErr to be %T got %T: %q",
    99  			outErr,
   100  			inErr,
   101  			inErr.Error(),
   102  		)
   103  	}
   104  
   105  	subErrorsVal, ok := md["suberrors"]
   106  	if ok {
   107  		if len(subErrorsVal) != 1 {
   108  			return berrors.InternalServerError(
   109  				"multiple 'suberrors' in metadata, wrapped error %q",
   110  				inErrMsg,
   111  			)
   112  		}
   113  
   114  		unquotedSubErrors, unquoteErr := strconv.Unquote(subErrorsVal[0])
   115  		if unquoteErr != nil {
   116  			return fmt.Errorf(
   117  				"unquoting 'suberrors' %q, wrapped error %q: %w",
   118  				subErrorsVal[0],
   119  				inErrMsg,
   120  				unquoteErr,
   121  			)
   122  		}
   123  
   124  		unmarshalErr := json.Unmarshal([]byte(unquotedSubErrors), &outErr.SubErrors)
   125  		if unmarshalErr != nil {
   126  			return berrors.InternalServerError(
   127  				"JSON unmarshaling 'suberrors' %q, wrapped error %q: %s",
   128  				subErrorsVal[0],
   129  				inErrMsg,
   130  				unmarshalErr,
   131  			)
   132  		}
   133  	}
   134  
   135  	retryAfterVal, ok := md["retryafter"]
   136  	if ok {
   137  		if len(retryAfterVal) != 1 {
   138  			return berrors.InternalServerError(
   139  				"multiple 'retryafter' in metadata, wrapped error %q",
   140  				inErrMsg,
   141  			)
   142  		}
   143  		var parseErr error
   144  		outErr.RetryAfter, parseErr = time.ParseDuration(retryAfterVal[0])
   145  		if parseErr != nil {
   146  			return berrors.InternalServerError(
   147  				"parsing 'retryafter' as int64, wrapped error %q, parsing error: %s",
   148  				inErrMsg,
   149  				parseErr,
   150  			)
   151  		}
   152  	}
   153  	return outErr
   154  }