apierror.go raw

   1  // Copyright 2021, Google Inc.
   2  // All rights reserved.
   3  //
   4  // Redistribution and use in source and binary forms, with or without
   5  // modification, are permitted provided that the following conditions are
   6  // met:
   7  //
   8  //     * Redistributions of source code must retain the above copyright
   9  // notice, this list of conditions and the following disclaimer.
  10  //     * Redistributions in binary form must reproduce the above
  11  // copyright notice, this list of conditions and the following disclaimer
  12  // in the documentation and/or other materials provided with the
  13  // distribution.
  14  //     * Neither the name of Google Inc. nor the names of its
  15  // contributors may be used to endorse or promote products derived from
  16  // this software without specific prior written permission.
  17  //
  18  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  19  // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  20  // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  21  // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  22  // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  23  // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  24  // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  25  // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  26  // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  27  // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  28  // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29  
  30  // Package apierror implements a wrapper error for parsing error details from
  31  // API calls. Both HTTP & gRPC status errors are supported.
  32  //
  33  // For examples of how to use [APIError] with client libraries please reference
  34  // [Inspecting errors](https://pkg.go.dev/cloud.google.com/go#hdr-Inspecting_errors)
  35  // in the client library documentation.
  36  package apierror
  37  
  38  import (
  39  	"errors"
  40  	"fmt"
  41  	"net/http"
  42  	"strings"
  43  
  44  	jsonerror "github.com/googleapis/gax-go/v2/apierror/internal/proto"
  45  	"google.golang.org/api/googleapi"
  46  	"google.golang.org/genproto/googleapis/rpc/errdetails"
  47  	"google.golang.org/grpc/codes"
  48  	"google.golang.org/grpc/status"
  49  	"google.golang.org/protobuf/encoding/protojson"
  50  	"google.golang.org/protobuf/proto"
  51  )
  52  
  53  // canonicalMap maps HTTP codes to gRPC status code equivalents.
  54  var canonicalMap = map[int]codes.Code{
  55  	http.StatusOK:                           codes.OK,
  56  	http.StatusBadRequest:                   codes.InvalidArgument,
  57  	http.StatusForbidden:                    codes.PermissionDenied,
  58  	http.StatusNotFound:                     codes.NotFound,
  59  	http.StatusConflict:                     codes.Aborted,
  60  	http.StatusRequestedRangeNotSatisfiable: codes.OutOfRange,
  61  	http.StatusTooManyRequests:              codes.ResourceExhausted,
  62  	http.StatusGatewayTimeout:               codes.DeadlineExceeded,
  63  	http.StatusNotImplemented:               codes.Unimplemented,
  64  	http.StatusServiceUnavailable:           codes.Unavailable,
  65  	http.StatusUnauthorized:                 codes.Unauthenticated,
  66  }
  67  
  68  // toCode maps an http code to the most correct equivalent.
  69  func toCode(httpCode int) codes.Code {
  70  	if sCode, ok := canonicalMap[httpCode]; ok {
  71  		return sCode
  72  	}
  73  	switch {
  74  	case httpCode >= 200 && httpCode < 300:
  75  		return codes.OK
  76  
  77  	case httpCode >= 400 && httpCode < 500:
  78  		return codes.FailedPrecondition
  79  
  80  	case httpCode >= 500 && httpCode < 600:
  81  		return codes.Internal
  82  	}
  83  	return codes.Unknown
  84  }
  85  
  86  // ErrDetails holds the google/rpc/error_details.proto messages.
  87  type ErrDetails struct {
  88  	ErrorInfo           *errdetails.ErrorInfo
  89  	BadRequest          *errdetails.BadRequest
  90  	PreconditionFailure *errdetails.PreconditionFailure
  91  	QuotaFailure        *errdetails.QuotaFailure
  92  	RetryInfo           *errdetails.RetryInfo
  93  	ResourceInfo        *errdetails.ResourceInfo
  94  	RequestInfo         *errdetails.RequestInfo
  95  	DebugInfo           *errdetails.DebugInfo
  96  	Help                *errdetails.Help
  97  	LocalizedMessage    *errdetails.LocalizedMessage
  98  
  99  	// Unknown stores unidentifiable error details.
 100  	Unknown []interface{}
 101  }
 102  
 103  // ErrMessageNotFound is used to signal ExtractProtoMessage found no matching messages.
 104  var ErrMessageNotFound = errors.New("message not found")
 105  
 106  // ExtractProtoMessage provides a mechanism for extracting protobuf messages from the
 107  // Unknown error details. If ExtractProtoMessage finds an unknown message of the same type,
 108  // the content of the message is copied to the provided message.
 109  //
 110  // ExtractProtoMessage will return ErrMessageNotFound if there are no message matching the
 111  // protocol buffer type of the provided message.
 112  func (e ErrDetails) ExtractProtoMessage(v proto.Message) error {
 113  	if v == nil {
 114  		return ErrMessageNotFound
 115  	}
 116  	for _, elem := range e.Unknown {
 117  		if elemProto, ok := elem.(proto.Message); ok {
 118  			if v.ProtoReflect().Type() == elemProto.ProtoReflect().Type() {
 119  				proto.Merge(v, elemProto)
 120  				return nil
 121  			}
 122  		}
 123  	}
 124  	return ErrMessageNotFound
 125  }
 126  
 127  func (e ErrDetails) String() string {
 128  	var d strings.Builder
 129  	if e.ErrorInfo != nil {
 130  		d.WriteString(fmt.Sprintf("error details: name = ErrorInfo reason = %s domain = %s metadata = %s\n",
 131  			e.ErrorInfo.GetReason(), e.ErrorInfo.GetDomain(), e.ErrorInfo.GetMetadata()))
 132  	}
 133  
 134  	if e.BadRequest != nil {
 135  		v := e.BadRequest.GetFieldViolations()
 136  		var f []string
 137  		var desc []string
 138  		for _, x := range v {
 139  			f = append(f, x.GetField())
 140  			desc = append(desc, x.GetDescription())
 141  		}
 142  		d.WriteString(fmt.Sprintf("error details: name = BadRequest field = %s desc = %s\n",
 143  			strings.Join(f, " "), strings.Join(desc, " ")))
 144  	}
 145  
 146  	if e.PreconditionFailure != nil {
 147  		v := e.PreconditionFailure.GetViolations()
 148  		var t []string
 149  		var s []string
 150  		var desc []string
 151  		for _, x := range v {
 152  			t = append(t, x.GetType())
 153  			s = append(s, x.GetSubject())
 154  			desc = append(desc, x.GetDescription())
 155  		}
 156  		d.WriteString(fmt.Sprintf("error details: name = PreconditionFailure type = %s subj = %s desc = %s\n", strings.Join(t, " "),
 157  			strings.Join(s, " "), strings.Join(desc, " ")))
 158  	}
 159  
 160  	if e.QuotaFailure != nil {
 161  		v := e.QuotaFailure.GetViolations()
 162  		var s []string
 163  		var desc []string
 164  		for _, x := range v {
 165  			s = append(s, x.GetSubject())
 166  			desc = append(desc, x.GetDescription())
 167  		}
 168  		d.WriteString(fmt.Sprintf("error details: name = QuotaFailure subj = %s desc = %s\n",
 169  			strings.Join(s, " "), strings.Join(desc, " ")))
 170  	}
 171  
 172  	if e.RequestInfo != nil {
 173  		d.WriteString(fmt.Sprintf("error details: name = RequestInfo id = %s data = %s\n",
 174  			e.RequestInfo.GetRequestId(), e.RequestInfo.GetServingData()))
 175  	}
 176  
 177  	if e.ResourceInfo != nil {
 178  		d.WriteString(fmt.Sprintf("error details: name = ResourceInfo type = %s resourcename = %s owner = %s desc = %s\n",
 179  			e.ResourceInfo.GetResourceType(), e.ResourceInfo.GetResourceName(),
 180  			e.ResourceInfo.GetOwner(), e.ResourceInfo.GetDescription()))
 181  
 182  	}
 183  	if e.RetryInfo != nil {
 184  		d.WriteString(fmt.Sprintf("error details: retry in %s\n", e.RetryInfo.GetRetryDelay().AsDuration()))
 185  
 186  	}
 187  	if e.Unknown != nil {
 188  		var s []string
 189  		for _, x := range e.Unknown {
 190  			s = append(s, fmt.Sprintf("%v", x))
 191  		}
 192  		d.WriteString(fmt.Sprintf("error details: name = Unknown  desc = %s\n", strings.Join(s, " ")))
 193  	}
 194  
 195  	if e.DebugInfo != nil {
 196  		d.WriteString(fmt.Sprintf("error details: name = DebugInfo detail = %s stack = %s\n", e.DebugInfo.GetDetail(),
 197  			strings.Join(e.DebugInfo.GetStackEntries(), " ")))
 198  	}
 199  	if e.Help != nil {
 200  		var desc []string
 201  		var url []string
 202  		for _, x := range e.Help.Links {
 203  			desc = append(desc, x.GetDescription())
 204  			url = append(url, x.GetUrl())
 205  		}
 206  		d.WriteString(fmt.Sprintf("error details: name = Help desc = %s url = %s\n",
 207  			strings.Join(desc, " "), strings.Join(url, " ")))
 208  	}
 209  	if e.LocalizedMessage != nil {
 210  		d.WriteString(fmt.Sprintf("error details: name = LocalizedMessage locale = %s msg = %s\n",
 211  			e.LocalizedMessage.GetLocale(), e.LocalizedMessage.GetMessage()))
 212  	}
 213  
 214  	return d.String()
 215  }
 216  
 217  // APIError wraps either a gRPC Status error or a HTTP googleapi.Error. It
 218  // implements error and Status interfaces.
 219  type APIError struct {
 220  	err     error
 221  	status  *status.Status
 222  	httpErr *googleapi.Error
 223  	details ErrDetails
 224  }
 225  
 226  // Details presents the error details of the APIError.
 227  func (a *APIError) Details() ErrDetails {
 228  	return a.details
 229  }
 230  
 231  // Unwrap extracts the original error.
 232  func (a *APIError) Unwrap() error {
 233  	return a.err
 234  }
 235  
 236  // Error returns a readable representation of the APIError.
 237  func (a *APIError) Error() string {
 238  	var msg string
 239  	if a.httpErr != nil {
 240  		// Truncate the googleapi.Error message because it dumps the Details in
 241  		// an ugly way.
 242  		msg = fmt.Sprintf("googleapi: Error %d: %s", a.httpErr.Code, a.httpErr.Message)
 243  	} else if a.status != nil && a.err != nil {
 244  		msg = a.err.Error()
 245  	} else if a.status != nil {
 246  		msg = a.status.Message()
 247  	}
 248  	return strings.TrimSpace(fmt.Sprintf("%s\n%s", msg, a.details))
 249  }
 250  
 251  // GRPCStatus extracts the underlying gRPC Status error.
 252  // This method is necessary to fulfill the interface
 253  // described in https://pkg.go.dev/google.golang.org/grpc/status#FromError.
 254  //
 255  // For errors that originated as an HTTP-based googleapi.Error, GRPCStatus()
 256  // returns a status that attempts to map from the original HTTP code to an
 257  // equivalent gRPC status code.  For use cases where you want to avoid this
 258  // behavior, error unwrapping can be used.
 259  func (a *APIError) GRPCStatus() *status.Status {
 260  	return a.status
 261  }
 262  
 263  // Reason returns the reason in an ErrorInfo.
 264  // If ErrorInfo is nil, it returns an empty string.
 265  func (a *APIError) Reason() string {
 266  	return a.details.ErrorInfo.GetReason()
 267  }
 268  
 269  // Domain returns the domain in an ErrorInfo.
 270  // If ErrorInfo is nil, it returns an empty string.
 271  func (a *APIError) Domain() string {
 272  	return a.details.ErrorInfo.GetDomain()
 273  }
 274  
 275  // Metadata returns the metadata in an ErrorInfo.
 276  // If ErrorInfo is nil, it returns nil.
 277  func (a *APIError) Metadata() map[string]string {
 278  	return a.details.ErrorInfo.GetMetadata()
 279  
 280  }
 281  
 282  // setDetailsFromError parses a Status error or a googleapi.Error
 283  // and sets status and details or httpErr and details, respectively.
 284  // It returns false if neither Status nor googleapi.Error can be parsed.
 285  //
 286  // When err is a googleapi.Error, the status of the returned error will be
 287  // mapped to the closest equivalent gGRPC status code.
 288  func (a *APIError) setDetailsFromError(err error) bool {
 289  	st, isStatus := status.FromError(err)
 290  	var herr *googleapi.Error
 291  	isHTTPErr := errors.As(err, &herr)
 292  
 293  	switch {
 294  	case isStatus:
 295  		a.status = st
 296  		a.details = parseDetails(st.Details())
 297  	case isHTTPErr:
 298  		a.httpErr = herr
 299  		a.details = parseHTTPDetails(herr)
 300  		a.status = status.New(toCode(a.httpErr.Code), herr.Message)
 301  	default:
 302  		return false
 303  	}
 304  	return true
 305  }
 306  
 307  // FromError parses a Status error or a googleapi.Error and builds an
 308  // APIError, wrapping the provided error in the new APIError. It
 309  // returns false if neither Status nor googleapi.Error can be parsed.
 310  func FromError(err error) (*APIError, bool) {
 311  	return ParseError(err, true)
 312  }
 313  
 314  // ParseError parses a Status error or a googleapi.Error and builds an
 315  // APIError. If wrap is true, it wraps the error in the new APIError.
 316  // It returns false if neither Status nor googleapi.Error can be parsed.
 317  func ParseError(err error, wrap bool) (*APIError, bool) {
 318  	if err == nil {
 319  		return nil, false
 320  	}
 321  	ae := APIError{}
 322  	if wrap {
 323  		ae = APIError{err: err}
 324  	}
 325  	if !ae.setDetailsFromError(err) {
 326  		return nil, false
 327  	}
 328  	return &ae, true
 329  }
 330  
 331  // parseDetails accepts a slice of interface{} that should be backed by some
 332  // sort of proto.Message that can be cast to the google/rpc/error_details.proto
 333  // types.
 334  //
 335  // This is for internal use only.
 336  func parseDetails(details []interface{}) ErrDetails {
 337  	var ed ErrDetails
 338  	for _, d := range details {
 339  		switch d := d.(type) {
 340  		case *errdetails.ErrorInfo:
 341  			ed.ErrorInfo = d
 342  		case *errdetails.BadRequest:
 343  			ed.BadRequest = d
 344  		case *errdetails.PreconditionFailure:
 345  			ed.PreconditionFailure = d
 346  		case *errdetails.QuotaFailure:
 347  			ed.QuotaFailure = d
 348  		case *errdetails.RetryInfo:
 349  			ed.RetryInfo = d
 350  		case *errdetails.ResourceInfo:
 351  			ed.ResourceInfo = d
 352  		case *errdetails.RequestInfo:
 353  			ed.RequestInfo = d
 354  		case *errdetails.DebugInfo:
 355  			ed.DebugInfo = d
 356  		case *errdetails.Help:
 357  			ed.Help = d
 358  		case *errdetails.LocalizedMessage:
 359  			ed.LocalizedMessage = d
 360  		default:
 361  			ed.Unknown = append(ed.Unknown, d)
 362  		}
 363  	}
 364  
 365  	return ed
 366  }
 367  
 368  // parseHTTPDetails will convert the given googleapi.Error into the protobuf
 369  // representation then parse the Any values that contain the error details.
 370  //
 371  // This is for internal use only.
 372  func parseHTTPDetails(gae *googleapi.Error) ErrDetails {
 373  	e := &jsonerror.Error{}
 374  	if err := protojson.Unmarshal([]byte(gae.Body), e); err != nil {
 375  		// If the error body does not conform to the error schema, ignore it
 376  		// altogther. See https://cloud.google.com/apis/design/errors#http_mapping.
 377  		return ErrDetails{}
 378  	}
 379  
 380  	// Coerce the Any messages into proto.Message then parse the details.
 381  	details := []interface{}{}
 382  	for _, any := range e.GetError().GetDetails() {
 383  		m, err := any.UnmarshalNew()
 384  		if err != nil {
 385  			// Ignore malformed Any values.
 386  			continue
 387  		}
 388  		details = append(details, m)
 389  	}
 390  
 391  	return parseDetails(details)
 392  }
 393  
 394  // HTTPCode returns the underlying HTTP response status code. This method returns
 395  // `-1` if the underlying error is a [google.golang.org/grpc/status.Status]. To
 396  // check gRPC error codes use [google.golang.org/grpc/status.Code].
 397  func (a *APIError) HTTPCode() int {
 398  	if a.httpErr == nil {
 399  		return -1
 400  	}
 401  	return a.httpErr.Code
 402  }
 403