go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/grpc/grpcutil/errors.go (about) 1 // Copyright 2016 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package grpcutil 16 17 import ( 18 "context" 19 "net/http" 20 21 "google.golang.org/grpc/codes" 22 "google.golang.org/grpc/status" 23 24 "go.chromium.org/luci/common/errors" 25 "go.chromium.org/luci/common/retry/transient" 26 ) 27 28 // WrapIfTransient wraps the supplied gRPC error with a transient wrapper if 29 // its gRPC code is transient as determined by IsTransientCode. 30 // 31 // If the supplied error is nil, nil will be returned. 32 // 33 // Note that non-gRPC errors will have code codes.Unknown, which is considered 34 // transient, and be wrapped. This function should only be used on gRPC errors. 35 // 36 // Also note that codes.DeadlineExceeded is not considered a transient error, 37 // since it is often non-retriable (e.g. if the root context has expired, no 38 // amount of retries will resolve codes.DeadlineExceeded error). 39 func WrapIfTransient(err error) error { 40 if err == nil { 41 return nil 42 } 43 44 if IsTransientCode(Code(err)) { 45 return transient.Tag.Apply(err) 46 } 47 48 return err 49 } 50 51 // WrapIfTransientOr wraps the supplied gRPC error with a transient wrapper if 52 // its gRPC code is transient as determined by IsTransientCode or matches any 53 // of given `extra` codes. 54 // 55 // See WrapIfTransient for other caveats. 56 func WrapIfTransientOr(err error, extra ...codes.Code) error { 57 if err == nil { 58 return nil 59 } 60 61 code := Code(err) 62 if IsTransientCode(code) { 63 return transient.Tag.Apply(err) 64 } 65 66 for _, c := range extra { 67 if code == c { 68 return transient.Tag.Apply(err) 69 } 70 } 71 72 return err 73 } 74 75 type grpcCodeTag struct{ Key errors.TagKey } 76 77 func (g grpcCodeTag) With(code codes.Code) errors.TagValue { 78 return errors.TagValue{Key: g.Key, Value: code} 79 } 80 func (g grpcCodeTag) In(err error) (v codes.Code, ok bool) { 81 d, ok := errors.TagValueIn(g.Key, err) 82 if ok { 83 v = d.(codes.Code) 84 } 85 return 86 } 87 88 // Tag may be used to associate a gRPC status code with this error. 89 // 90 // The tag value MUST be a "google.golang.org/grpc/codes".Code. 91 var Tag = grpcCodeTag{errors.NewTagKey("gRPC Code")} 92 93 // Shortcuts for assigning tags with codes known at compile time. 94 // 95 // Instead errors.Annotate(...).Tag(grpcutil.Tag.With(codes.InvalidArgument)) do 96 // errors.Annotate(...).Tag(grpcutil.InvalidArgumentTag)). 97 var ( 98 CanceledTag = Tag.With(codes.Canceled) 99 UnknownTag = Tag.With(codes.Unknown) 100 InvalidArgumentTag = Tag.With(codes.InvalidArgument) 101 DeadlineExceededTag = Tag.With(codes.DeadlineExceeded) 102 NotFoundTag = Tag.With(codes.NotFound) 103 AlreadyExistsTag = Tag.With(codes.AlreadyExists) 104 PermissionDeniedTag = Tag.With(codes.PermissionDenied) 105 UnauthenticatedTag = Tag.With(codes.Unauthenticated) 106 ResourceExhaustedTag = Tag.With(codes.ResourceExhausted) 107 FailedPreconditionTag = Tag.With(codes.FailedPrecondition) 108 AbortedTag = Tag.With(codes.Aborted) 109 OutOfRangeTag = Tag.With(codes.OutOfRange) 110 UnimplementedTag = Tag.With(codes.Unimplemented) 111 InternalTag = Tag.With(codes.Internal) 112 UnavailableTag = Tag.With(codes.Unavailable) 113 DataLossTag = Tag.With(codes.DataLoss) 114 ) 115 116 // codeToStatus maps gRPC codes to HTTP statuses. 117 // Based on https://cloud.google.com/apis/design/errors 118 var codeToStatus = map[codes.Code]int{ 119 codes.OK: http.StatusOK, 120 codes.Canceled: 499, 121 codes.InvalidArgument: http.StatusBadRequest, 122 codes.DataLoss: http.StatusInternalServerError, 123 codes.Internal: http.StatusInternalServerError, 124 codes.Unknown: http.StatusInternalServerError, 125 codes.DeadlineExceeded: http.StatusGatewayTimeout, 126 codes.NotFound: http.StatusNotFound, 127 codes.AlreadyExists: http.StatusConflict, 128 codes.PermissionDenied: http.StatusForbidden, 129 codes.Unauthenticated: http.StatusUnauthorized, 130 codes.ResourceExhausted: http.StatusTooManyRequests, 131 codes.FailedPrecondition: http.StatusBadRequest, 132 codes.OutOfRange: http.StatusBadRequest, 133 codes.Unimplemented: http.StatusNotImplemented, 134 codes.Unavailable: http.StatusServiceUnavailable, 135 codes.Aborted: http.StatusConflict, 136 } 137 138 // CodeStatus maps gRPC codes to HTTP status codes. 139 // 140 // Falls back to http.StatusInternalServerError if the code is unrecognized. 141 func CodeStatus(code codes.Code) int { 142 if status, ok := codeToStatus[code]; ok { 143 return status 144 } 145 return http.StatusInternalServerError 146 } 147 148 // Code returns the gRPC code for a given error. 149 // 150 // In addition to the functionality of status.Code, this will unwrap any wrapped 151 // errors before asking for its code. 152 // 153 // If the error is a MultiError containing more than one type of error code, 154 // this will return codes.Unknown. 155 func Code(err error) codes.Code { 156 if code, ok := Tag.In(err); ok { 157 return code 158 } 159 // If it's a multi-error, see if all errors have the same code. 160 // Otherwise return codes.Unknown. 161 if multi, isMulti := err.(errors.MultiError); isMulti { 162 code := codes.OK 163 for _, err := range multi { 164 nextCode := Code(err) 165 if code == codes.OK { // unset 166 code = nextCode 167 continue 168 } 169 if nextCode != code { 170 return codes.Unknown 171 } 172 } 173 return code 174 } 175 return status.Code(errors.Unwrap(err)) 176 } 177 178 // IsTransientCode returns true if a given gRPC code is codes.Internal, 179 // codes.Unknown or codes.Unavailable. 180 func IsTransientCode(code codes.Code) bool { 181 switch code { 182 case codes.Internal, codes.Unknown, codes.Unavailable: 183 return true 184 185 default: 186 return false 187 } 188 } 189 190 // GRPCifyAndLogErr converts an annotated LUCI error to a gRPC error and logs 191 // internal details (including stack trace) for errors with Internal or Unknown 192 // codes. 193 // 194 // If err is already gRPC error (or nil), it is silently passed through, even 195 // if it is Internal. There's nothing interesting to log in this case. 196 // 197 // Intended to be used in defer section of gRPC handlers like so: 198 // 199 // func (...) Method(...) (resp *pb.Response, err error) { 200 // defer func() { err = grpcutil.GRPCifyAndLogErr(c, err) }() 201 // ... 202 // } 203 func GRPCifyAndLogErr(ctx context.Context, err error) error { 204 if err == nil { 205 return nil 206 } 207 if _, yep := status.FromError(err); yep { 208 return err 209 } 210 code := Code(err) 211 if code == codes.Internal || code == codes.Unknown { 212 errors.Log(ctx, err) 213 } 214 return status.Error(code, err.Error()) 215 }