go.uber.org/yarpc@v1.72.1/encoding/protobuf/v2/error.go (about) 1 // Copyright (c) 2022 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package v2 22 23 import ( 24 "errors" 25 "fmt" 26 "google.golang.org/protobuf/encoding/prototext" 27 "strings" 28 29 "github.com/golang/protobuf/ptypes/any" 30 "go.uber.org/yarpc/api/transport" 31 "go.uber.org/yarpc/internal/grpcerrorcodes" 32 "go.uber.org/yarpc/yarpcerrors" 33 rpc_status "google.golang.org/genproto/googleapis/rpc/status" 34 "google.golang.org/grpc/status" 35 "google.golang.org/protobuf/proto" 36 "google.golang.org/protobuf/types/known/anypb" 37 ) 38 39 const ( 40 // format for converting error details to string 41 _errDetailsFmt = "[]{ %s }" 42 // format for converting a single message to string 43 _errDetailFmt = "%s{%s}" 44 ) 45 46 var _ error = (*pberror)(nil) 47 48 type pberror struct { 49 code yarpcerrors.Code 50 message string 51 details []*any.Any 52 } 53 54 func (err *pberror) Error() string { 55 var b strings.Builder 56 b.WriteString("code:") 57 b.WriteString(err.code.String()) 58 if err.message != "" { 59 b.WriteString(" message:") 60 b.WriteString(err.message) 61 } 62 return b.String() 63 } 64 65 // NewError returns a new YARPC protobuf error. To access the error's fields, 66 // use the yarpcerrors package APIs for the code and message, and the 67 // `GetErrorDetails(error)` function for error details. The `yarpcerrors.Details()` 68 // will not work on this error. 69 // 70 // If the Code is CodeOK, this will return nil. 71 func NewError(code yarpcerrors.Code, message string, options ...ErrorOption) error { 72 if code == yarpcerrors.CodeOK { 73 return nil 74 } 75 pbErr := &pberror{ 76 code: code, 77 message: message, 78 } 79 for _, opt := range options { 80 if err := opt.apply(pbErr); err != nil { 81 return err 82 } 83 } 84 return pbErr 85 } 86 87 // GetErrorDetails returns the error details of the error. 88 // 89 // This method supports extracting details from wrapped errors. 90 // 91 // Each element in the returned slice of interface{} is either a proto.Message 92 // or an error to explain why the element is not a proto.Message, most likely 93 // because the error detail could not be unmarshaled. 94 func GetErrorDetails(err error) []interface{} { 95 var target *pberror 96 if errors.As(err, &target) && len(target.details) > 0 { 97 results := make([]interface{}, 0, len(target.details)) 98 for _, ghAny := range target.details { 99 detail, err := ghAny.UnmarshalNew() 100 if err != nil { 101 results = append(results, err) 102 continue 103 } 104 results = append(results, detail) 105 } 106 return results 107 } 108 return nil 109 } 110 111 // ErrorOption is an option for the NewError constructor. 112 type ErrorOption struct{ apply func(*pberror) error } 113 114 // WithErrorDetails adds to the details of the error. 115 // If any errors are encountered, it returns the first error encountered. 116 func WithErrorDetails(details ...proto.Message) ErrorOption { 117 return ErrorOption{func(err *pberror) error { 118 for _, detail := range details { 119 any, terr := anypb.New(detail) 120 if terr != nil { 121 return terr 122 } 123 err.details = append(err.details, any) 124 } 125 return nil 126 }} 127 } 128 129 // convertToYARPCError is to be used for handling errors on the inbound side. 130 func convertToYARPCError(encoding transport.Encoding, err error, codec *codec, resw transport.ResponseWriter) error { 131 if err == nil { 132 return nil 133 } 134 var pberr *pberror 135 if errors.As(err, &pberr) { 136 setApplicationErrorMeta(pberr, resw) 137 status, sterr := createStatusWithDetail(pberr, encoding, codec) 138 if sterr != nil { 139 return sterr 140 } 141 return status 142 } 143 return err 144 } 145 146 func createStatusWithDetail(pberr *pberror, encoding transport.Encoding, codec *codec) (*yarpcerrors.Status, error) { 147 if pberr.code == yarpcerrors.CodeOK { 148 return nil, errors.New("no status error for error with code OK") 149 } 150 151 st := status.New(grpcerrorcodes.YARPCCodeToGRPCCode[pberr.code], pberr.message) 152 // Here we check that status.New has returned a valid error. 153 if st.Err() == nil { 154 return nil, fmt.Errorf("no status error for error with code %d", pberr.code) 155 } 156 pst := st.Proto() 157 pst.Details = pberr.details 158 159 detailsBytes, cleanup, marshalErr := marshal(encoding, pst, codec) 160 if marshalErr != nil { 161 return nil, marshalErr 162 } 163 defer cleanup() 164 yarpcDet := make([]byte, len(detailsBytes)) 165 copy(yarpcDet, detailsBytes) 166 return yarpcerrors.Newf(pberr.code, pberr.message).WithDetails(yarpcDet), nil 167 } 168 169 func setApplicationErrorMeta(pberr *pberror, resw transport.ResponseWriter) { 170 applicationErrorMetaSetter, ok := resw.(transport.ApplicationErrorMetaSetter) 171 if !ok { 172 return 173 } 174 175 var ( 176 decodedDetails = GetErrorDetails(pberr) 177 178 appErrName string 179 details = make([]string, 0, len(decodedDetails)) 180 ) 181 182 for _, detail := range decodedDetails { 183 if m, ok := detail.(proto.Message); ok { 184 if appErrName == "" { 185 // only grab the first name since this will be emitted with metrics 186 appErrName = messageNameWithoutPackage(string(proto.MessageName(m))) 187 } 188 details = append(details, protobufMessageToString(detail.(proto.Message))) 189 } 190 } 191 192 applicationErrorMetaSetter.SetApplicationErrorMeta(&transport.ApplicationErrorMeta{ 193 Name: appErrName, 194 Details: fmt.Sprintf(_errDetailsFmt, strings.Join(details, " , ")), 195 }) 196 } 197 198 // messageNameWithoutPackage strips the package name, returning just the type 199 // name. 200 // 201 // For example: 202 // 203 // uber.foo.bar.TypeName -> TypeName 204 func messageNameWithoutPackage(messageName string) string { 205 if i := strings.LastIndex(messageName, "."); i >= 0 { 206 return messageName[i+1:] 207 } 208 return messageName 209 } 210 211 func protobufMessageToString(message proto.Message) string { 212 return fmt.Sprintf(_errDetailFmt, 213 messageNameWithoutPackage(string(proto.MessageName(message))), 214 prototext.MarshalOptions{}.Format(message)) 215 } 216 217 // convertFromYARPCError is to be used for handling errors on the outbound side. 218 func convertFromYARPCError(encoding transport.Encoding, err error, codec *codec) error { 219 if err == nil || !yarpcerrors.IsStatus(err) { 220 return err 221 } 222 yarpcErr := yarpcerrors.FromError(err) 223 if yarpcErr.Details() == nil { 224 return err 225 } 226 st := &rpc_status.Status{} 227 unmarshalErr := unmarshalBytes(encoding, yarpcErr.Details(), st, codec) 228 if unmarshalErr != nil { 229 return unmarshalErr 230 } 231 232 return newErrorWithDetails(yarpcErr.Code(), yarpcErr.Message(), st.GetDetails()) 233 } 234 235 func newErrorWithDetails(code yarpcerrors.Code, message string, details []*any.Any) error { 236 return &pberror{ 237 code: code, 238 message: message, 239 details: details, 240 } 241 } 242 243 func (err *pberror) YARPCError() *yarpcerrors.Status { 244 if err == nil { 245 return nil 246 } 247 status, statusErr := createStatusWithDetail(err, Encoding, newCodec(nil)) 248 if statusErr != nil { 249 return yarpcerrors.FromError(statusErr) 250 } 251 return status 252 }