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  }