go.uber.org/yarpc@v1.72.1/encoding/protobuf/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 protobuf 22 23 import ( 24 "errors" 25 "fmt" 26 "strings" 27 28 "github.com/gogo/googleapis/google/rpc" 29 "github.com/gogo/protobuf/proto" 30 "github.com/gogo/protobuf/types" 31 "github.com/gogo/status" 32 "go.uber.org/yarpc/api/transport" 33 "go.uber.org/yarpc/internal/grpcerrorcodes" 34 "go.uber.org/yarpc/yarpcerrors" 35 ) 36 37 const ( 38 // format for converting error details to string 39 _errDetailsFmt = "[]{ %s }" 40 // format for converting a single message to string 41 _errDetailFmt = "%s{%s}" 42 ) 43 44 var _ error = (*pberror)(nil) 45 46 type pberror struct { 47 code yarpcerrors.Code 48 message string 49 details []*types.Any 50 } 51 52 func (err *pberror) Error() string { 53 var b strings.Builder 54 b.WriteString("code:") 55 b.WriteString(err.code.String()) 56 if err.message != "" { 57 b.WriteString(" message:") 58 b.WriteString(err.message) 59 } 60 return b.String() 61 } 62 63 // NewError returns a new YARPC protobuf error. To access the error's fields, 64 // use the yarpcerrors package APIs for the code and message, and the 65 // `GetErrorDetails(error)` function for error details. The `yarpcerrors.Details()` 66 // will not work on this error. 67 // 68 // If the Code is CodeOK, this will return nil. 69 func NewError(code yarpcerrors.Code, message string, options ...ErrorOption) error { 70 if code == yarpcerrors.CodeOK { 71 return nil 72 } 73 pbErr := &pberror{ 74 code: code, 75 message: message, 76 } 77 for _, opt := range options { 78 if err := opt.apply(pbErr); err != nil { 79 return err 80 } 81 } 82 return pbErr 83 } 84 85 // GetErrorDetails returns the error details of the error. 86 // 87 // This method supports extracting details from wrapped errors. 88 // 89 // Each element in the returned slice of interface{} is either a proto.Message 90 // or an error to explain why the element is not a proto.Message, most likely 91 // because the error detail could not be unmarshaled. 92 // See: https://github.com/gogo/status/blob/master/status.go#L193 93 func GetErrorDetails(err error) []interface{} { 94 if err == nil { 95 return nil 96 } 97 var target *pberror 98 if errors.As(err, &target) && len(target.details) > 0 { 99 results := make([]interface{}, 0, len(target.details)) 100 for _, any := range target.details { 101 detail := &types.DynamicAny{} 102 if err := types.UnmarshalAny(any, detail); err != nil { 103 results = append(results, err) 104 continue 105 } 106 results = append(results, detail.Message) 107 } 108 return results 109 } 110 return nil 111 } 112 113 // ErrorOption is an option for the NewError constructor. 114 type ErrorOption struct{ apply func(*pberror) error } 115 116 // WithErrorDetails adds to the details of the error. 117 // If any errors are encountered, it returns the first error encountered. 118 // See: https://github.com/gogo/status/blob/master/status.go#L175 119 func WithErrorDetails(details ...proto.Message) ErrorOption { 120 return ErrorOption{func(err *pberror) error { 121 for _, detail := range details { 122 any, terr := types.MarshalAny(detail) 123 if terr != nil { 124 return terr 125 } 126 err.details = append(err.details, any) 127 } 128 return nil 129 }} 130 } 131 132 // convertToYARPCError is to be used for handling errors on the inbound side. 133 func convertToYARPCError(encoding transport.Encoding, err error, codec *codec, resw transport.ResponseWriter) error { 134 if err == nil { 135 return nil 136 } 137 var pberr *pberror 138 if errors.As(err, &pberr) { 139 setApplicationErrorMeta(pberr, resw) 140 status, sterr := createStatusWithDetail(pberr, encoding, codec) 141 if sterr != nil { 142 return sterr 143 } 144 return status 145 } 146 return err 147 } 148 149 func createStatusWithDetail(pberr *pberror, encoding transport.Encoding, codec *codec) (*yarpcerrors.Status, error) { 150 if pberr.code == yarpcerrors.CodeOK { 151 return nil, errors.New("no status error for error with code OK") 152 } 153 154 st := status.New(grpcerrorcodes.YARPCCodeToGRPCCode[pberr.code], pberr.message) 155 // Here we check that status.New has returned a valid error. 156 if st.Err() == nil { 157 return nil, fmt.Errorf("no status error for error with code %d", pberr.code) 158 } 159 pst := st.Proto() 160 pst.Details = pberr.details 161 162 detailsBytes, cleanup, marshalErr := marshal(encoding, pst, codec) 163 if marshalErr != nil { 164 return nil, marshalErr 165 } 166 defer cleanup() 167 yarpcDet := make([]byte, len(detailsBytes)) 168 copy(yarpcDet, detailsBytes) 169 return yarpcerrors.Newf(pberr.code, pberr.message).WithDetails(yarpcDet), nil 170 } 171 172 func setApplicationErrorMeta(pberr *pberror, resw transport.ResponseWriter) { 173 applicationErrorMetaSetter, ok := resw.(transport.ApplicationErrorMetaSetter) 174 if !ok { 175 return 176 } 177 178 var ( 179 decodedDetails = GetErrorDetails(pberr) 180 181 appErrName string 182 details = make([]string, 0, len(decodedDetails)) 183 ) 184 185 for _, detail := range decodedDetails { 186 if m, ok := detail.(proto.Message); ok { 187 if appErrName == "" { 188 // only grab the first name since this will be emitted with metrics 189 appErrName = messageNameWithoutPackage(proto.MessageName(m)) 190 } 191 details = append(details, protobufMessageToString(detail.(proto.Message))) 192 } 193 } 194 195 applicationErrorMetaSetter.SetApplicationErrorMeta(&transport.ApplicationErrorMeta{ 196 Name: appErrName, 197 Details: fmt.Sprintf(_errDetailsFmt, strings.Join(details, " , ")), 198 }) 199 } 200 201 // messageNameWithoutPackage strips the package name, returning just the type 202 // name. 203 // 204 // For example: 205 // 206 // uber.foo.bar.TypeName -> TypeName 207 func messageNameWithoutPackage(messageName string) string { 208 if i := strings.LastIndex(messageName, "."); i >= 0 { 209 return messageName[i+1:] 210 } 211 return messageName 212 } 213 214 func protobufMessageToString(message proto.Message) string { 215 return fmt.Sprintf(_errDetailFmt, 216 messageNameWithoutPackage(proto.MessageName(message)), 217 proto.CompactTextString(message)) 218 } 219 220 // convertFromYARPCError is to be used for handling errors on the outbound side. 221 func convertFromYARPCError(encoding transport.Encoding, err error, codec *codec) error { 222 if err == nil || !yarpcerrors.IsStatus(err) { 223 return err 224 } 225 yarpcErr := yarpcerrors.FromError(err) 226 if yarpcErr.Details() == nil { 227 return err 228 } 229 st := &rpc.Status{} 230 unmarshalErr := unmarshalBytes(encoding, yarpcErr.Details(), st, codec) 231 if unmarshalErr != nil { 232 return unmarshalErr 233 } 234 235 return newErrorWithDetails(yarpcErr.Code(), yarpcErr.Message(), st.GetDetails()) 236 } 237 238 func newErrorWithDetails(code yarpcerrors.Code, message string, details []*types.Any) error { 239 return &pberror{ 240 code: code, 241 message: message, 242 details: details, 243 } 244 } 245 246 func (err *pberror) YARPCError() *yarpcerrors.Status { 247 if err == nil { 248 return nil 249 } 250 status, statusErr := createStatusWithDetail(err, Encoding, newCodec(nil)) 251 if statusErr != nil { 252 return yarpcerrors.FromError(statusErr) 253 } 254 return status 255 }