github.com/livekit/protocol@v1.39.3/utils/xtwirp/errors.go (about)

     1  package xtwirp
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"errors"
     7  
     8  	"github.com/twitchtv/twirp"
     9  	spb "google.golang.org/genproto/googleapis/rpc/status"
    10  	"google.golang.org/grpc/codes"
    11  	"google.golang.org/grpc/status"
    12  	"google.golang.org/protobuf/proto"
    13  )
    14  
    15  // DetailsMetaKey is a Twirp error metadata key that is used to pass protobuf-encoded error details.
    16  const DetailsMetaKey = "error_details"
    17  
    18  // ErrorMeta is an optional interface that allows attaching Twirp error metadata.
    19  type ErrorMeta interface {
    20  	TwirpErrorMeta() map[string]string
    21  }
    22  
    23  // ErrorCodeFromGRPC converts gRPC error code to Twirp.
    24  func ErrorCodeFromGRPC(code codes.Code) twirp.ErrorCode {
    25  	switch code {
    26  	case codes.OK:
    27  		return twirp.NoError
    28  	case codes.Canceled:
    29  		return twirp.Canceled
    30  	case codes.Unknown:
    31  		return twirp.Unknown
    32  	case codes.InvalidArgument:
    33  		return twirp.InvalidArgument
    34  	case codes.DeadlineExceeded:
    35  		return twirp.DeadlineExceeded
    36  	case codes.NotFound:
    37  		return twirp.NotFound
    38  	case codes.AlreadyExists:
    39  		return twirp.AlreadyExists
    40  	case codes.PermissionDenied:
    41  		return twirp.PermissionDenied
    42  	case codes.ResourceExhausted:
    43  		return twirp.ResourceExhausted
    44  	case codes.FailedPrecondition:
    45  		return twirp.FailedPrecondition
    46  	case codes.Aborted:
    47  		return twirp.Aborted
    48  	case codes.OutOfRange:
    49  		return twirp.OutOfRange
    50  	case codes.Unimplemented:
    51  		return twirp.Unimplemented
    52  	case codes.Internal:
    53  		return twirp.Internal
    54  	case codes.Unavailable:
    55  		return twirp.Unavailable
    56  	case codes.DataLoss:
    57  		return twirp.DataLoss
    58  	case codes.Unauthenticated:
    59  		return twirp.Unauthenticated
    60  	default:
    61  		return twirp.Unknown
    62  	}
    63  }
    64  
    65  // WithDetailsFrom sets gRPC/PSRPC error details from src as Twirp error metadata on dst.
    66  //
    67  // If error details implement ErrorMeta, their custom metadata fields will be included as well.
    68  func WithDetailsFrom(dst twirp.Error, src error) twirp.Error {
    69  	st, ok := status.FromError(src)
    70  	if !ok {
    71  		return dst
    72  	}
    73  	if dst.Code() == twirp.Unknown {
    74  		dst = twirp.NewError(ErrorCodeFromGRPC(st.Code()), dst.Error())
    75  	}
    76  	return WithDetailsFromStatus(dst, st)
    77  }
    78  
    79  // ToError converts any error to Twirp error, preserving error details.
    80  func ToError(err error) twirp.Error {
    81  	if e, ok := err.(twirp.Error); ok {
    82  		return e
    83  	}
    84  	e := twirp.NewError(twirp.Unknown, err.Error())
    85  	e = WithDetailsFrom(e, err)
    86  	return e
    87  }
    88  
    89  // WithDetailsFromStatus sets gRPC error details from status as Twirp error metadata on dst.
    90  //
    91  // If error details implement ErrorMeta, their custom metadata fields will be included as well.
    92  func WithDetailsFromStatus(dst twirp.Error, st *status.Status) twirp.Error {
    93  	if st == nil {
    94  		return dst
    95  	}
    96  	details := st.Details()
    97  	if len(details) == 0 {
    98  		return dst
    99  	}
   100  	for _, d := range details {
   101  		if m, ok := d.(ErrorMeta); ok {
   102  			for k, v := range m.TwirpErrorMeta() {
   103  				dst = dst.WithMeta(k, v)
   104  			}
   105  		}
   106  	}
   107  	p := st.Proto()
   108  	if len(p.Details) == 0 {
   109  		return dst
   110  	}
   111  	data, err := proto.Marshal(p)
   112  	if err != nil {
   113  		return dst
   114  	}
   115  	val := base64.StdEncoding.EncodeToString(data)
   116  	return dst.WithMeta(DetailsMetaKey, val)
   117  }
   118  
   119  // StatusFromError is an analog of gRPCs status.FromError, but it also considers
   120  // error details encoded in Twirp metadata.
   121  func StatusFromError(err error) (*status.Status, bool) {
   122  	if st, ok := status.FromError(err); ok {
   123  		return st, true
   124  	}
   125  	var e twirp.Error
   126  	if !errors.As(err, &e) {
   127  		return nil, false
   128  	}
   129  	val := e.Meta(DetailsMetaKey)
   130  	if val == "" {
   131  		return nil, false
   132  	}
   133  	data, err := base64.StdEncoding.DecodeString(val)
   134  	if err != nil {
   135  		return nil, false
   136  	}
   137  	var p spb.Status
   138  	if err := proto.Unmarshal(data, &p); err != nil {
   139  		return nil, false
   140  	}
   141  	return status.FromProto(&p), true
   142  }
   143  
   144  // ClientPassErrorDetails converts Twirp errors to gRPC errors, if possible.
   145  // This allows passing error details returned via gRPC/PSRPC by the backend server.
   146  func ClientPassErrorDetails() twirp.ClientOption {
   147  	return twirp.WithClientInterceptors(func(fnc twirp.Method) twirp.Method {
   148  		return func(ctx context.Context, req any) (any, error) {
   149  			resp, err := fnc(ctx, req)
   150  			if err != nil {
   151  				if st, ok := StatusFromError(err); ok && st != nil {
   152  					err = st.Err()
   153  				}
   154  			}
   155  			return resp, err
   156  		}
   157  	})
   158  }
   159  
   160  // ServerPassErrorDetails converts gRPC errors to Twirp errors.
   161  // It properly converts gRPC/PSRPC error codes and preserves custom error details.
   162  func ServerPassErrorDetails() twirp.ServerOption {
   163  	return twirp.WithServerInterceptors(func(fnc twirp.Method) twirp.Method {
   164  		return func(ctx context.Context, req any) (any, error) {
   165  			resp, err := fnc(ctx, req)
   166  			if err != nil {
   167  				err = ToError(err)
   168  			}
   169  			return resp, err
   170  		}
   171  	})
   172  }