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