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  }