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 }