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 }