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  }