1# Copyright 2014 Google LLC
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"""Exceptions raised by Google API core & clients.
16
17This module provides base classes for all errors raised by libraries based
18on :mod:`google.api_core`, including both HTTP and gRPC clients.
19"""
20
21from __future__ import absolute_import, unicode_literals
22
23import http
24import http.client
25import warnings
26from typing import Dict, Optional
27
28from google.rpc import error_details_pb2
29
30
31def _warn_could_not_import_grpcio_status():
32 warnings.warn(
33 "Please install grpcio-status to obtain helpful grpc error messages.",
34 ImportWarning,
35 ) # pragma: NO COVER
36
37
38try:
39 import grpc
40
41 try:
42 from grpc_status import rpc_status
43 except ImportError: # pragma: NO COVER
44 _warn_could_not_import_grpcio_status()
45 rpc_status = None
46except ImportError: # pragma: NO COVER
47 grpc = None
48
49# Lookup tables for mapping exceptions from HTTP and gRPC transports.
50# Populated by _GoogleAPICallErrorMeta
51_HTTP_CODE_TO_EXCEPTION: Dict[int, Exception] = {}
52_GRPC_CODE_TO_EXCEPTION: Dict[int, Exception] = {}
53
54# Additional lookup table to map integer status codes to grpc status code
55# grpc does not currently support initializing enums from ints
56# i.e., grpc.StatusCode(5) raises an error
57_INT_TO_GRPC_CODE = {}
58if grpc is not None: # pragma: no branch
59 for x in grpc.StatusCode:
60 _INT_TO_GRPC_CODE[x.value[0]] = x
61
62
63class GoogleAPIError(Exception):
64 """Base class for all exceptions raised by Google API Clients."""
65
66 pass
67
68
69class DuplicateCredentialArgs(GoogleAPIError):
70 """Raised when multiple credentials are passed."""
71
72 pass
73
74
75class RetryError(GoogleAPIError):
76 """Raised when a function has exhausted all of its available retries.
77
78 Args:
79 message (str): The exception message.
80 cause (Exception): The last exception raised when retrying the
81 function.
82 """
83
84 def __init__(self, message, cause):
85 super(RetryError, self).__init__(message)
86 self.message = message
87 self._cause = cause
88
89 @property
90 def cause(self):
91 """The last exception raised when retrying the function."""
92 return self._cause
93
94 def __str__(self):
95 return "{}, last exception: {}".format(self.message, self.cause)
96
97 def __reduce__(self):
98 # ``cause`` is a required constructor argument that isn't stored in
99 # ``self.args``, so the default ``BaseException.__reduce__`` can't
100 # rebuild the exception (e.g. when it's pickled across processes).
101 return (self.__class__, (self.message, self._cause), self.__dict__)
102
103
104class _GoogleAPICallErrorMeta(type):
105 """Metaclass for registering GoogleAPICallError subclasses."""
106
107 def __new__(mcs, name, bases, class_dict):
108 cls = type.__new__(mcs, name, bases, class_dict)
109 if cls.code is not None:
110 _HTTP_CODE_TO_EXCEPTION.setdefault(cls.code, cls)
111 if cls.grpc_status_code is not None:
112 _GRPC_CODE_TO_EXCEPTION.setdefault(cls.grpc_status_code, cls)
113 return cls
114
115
116class GoogleAPICallError(GoogleAPIError, metaclass=_GoogleAPICallErrorMeta):
117 """Base class for exceptions raised by calling API methods.
118
119 Args:
120 message (str): The exception message.
121 errors (Sequence[Any]): An optional list of error details.
122 details (Sequence[Any]): An optional list of objects defined in google.rpc.error_details.
123 response (Union[requests.Request, grpc.Call]): The response or
124 gRPC call metadata.
125 error_info (Union[error_details_pb2.ErrorInfo, None]): An optional object containing error info
126 (google.rpc.error_details.ErrorInfo).
127 """
128
129 code: Optional[int] = None
130 """Optional[int]: The HTTP status code associated with this error.
131
132 This may be ``None`` if the exception does not have a direct mapping
133 to an HTTP error.
134
135 See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
136 """
137
138 grpc_status_code: Optional["grpc.StatusCode"] = None
139 """Optional[grpc.StatusCode]: The gRPC status code associated with this
140 error.
141
142 This may be ``None`` if the exception does not match up to a gRPC error.
143 """
144
145 def __init__(self, message, errors=(), details=(), response=None, error_info=None):
146 super(GoogleAPICallError, self).__init__(message)
147 self.message = message
148 """str: The exception message."""
149 self._errors = errors
150 self._details = details
151 self._response = response
152 self._error_info = error_info
153
154 def __str__(self):
155 error_msg = "{} {}".format(self.code, self.message)
156 if self.details:
157 error_msg = "{} {}".format(error_msg, self.details)
158 # Note: This else condition can be removed once proposal A from
159 # b/284179390 is implemented.
160 else:
161 if self.errors:
162 errors = [
163 f"{error.code}: {error.message}"
164 for error in self.errors
165 if hasattr(error, "code") and hasattr(error, "message")
166 ]
167 if errors:
168 error_msg = "{} {}".format(error_msg, "\n".join(errors))
169 return error_msg
170
171 @property
172 def reason(self):
173 """The reason of the error.
174
175 Reference:
176 https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto#L112
177
178 Returns:
179 Union[str, None]: An optional string containing reason of the error.
180 """
181 if not self._error_info:
182 return None
183 if isinstance(self._error_info, dict):
184 return self._error_info.get("reason")
185 return self._error_info.reason
186
187 @property
188 def domain(self):
189 """The logical grouping to which the "reason" belongs.
190
191 Reference:
192 https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto#L112
193
194 Returns:
195 Union[str, None]: An optional string containing a logical grouping to which the "reason" belongs.
196 """
197 if not self._error_info:
198 return None
199 if isinstance(self._error_info, dict):
200 return self._error_info.get("domain")
201 return self._error_info.domain
202
203 @property
204 def metadata(self):
205 """Additional structured details about this error.
206
207 Reference:
208 https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto#L112
209
210 Returns:
211 Union[Dict[str, str], None]: An optional object containing structured details about the error.
212 """
213 if not self._error_info:
214 return None
215 if isinstance(self._error_info, dict):
216 return self._error_info.get("metadata")
217 return self._error_info.metadata
218
219 @property
220 def errors(self):
221 """Detailed error information.
222
223 Returns:
224 Sequence[Any]: A list of additional error details.
225 """
226 return list(self._errors)
227
228 @property
229 def details(self):
230 """Information contained in google.rpc.status.details.
231
232 Reference:
233 https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto
234 https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto
235
236 Returns:
237 Sequence[Any]: A list of structured objects from error_details.proto
238 """
239 return list(self._details)
240
241 @property
242 def response(self):
243 """Optional[Union[requests.Request, grpc.Call]]: The response or
244 gRPC call metadata."""
245 return self._response
246
247
248class Redirection(GoogleAPICallError):
249 """Base class for for all redirection (HTTP 3xx) responses."""
250
251
252class MovedPermanently(Redirection):
253 """Exception mapping a ``301 Moved Permanently`` response."""
254
255 code = http.client.MOVED_PERMANENTLY
256
257
258class NotModified(Redirection):
259 """Exception mapping a ``304 Not Modified`` response."""
260
261 code = http.client.NOT_MODIFIED
262
263
264class TemporaryRedirect(Redirection):
265 """Exception mapping a ``307 Temporary Redirect`` response."""
266
267 code = http.client.TEMPORARY_REDIRECT
268
269
270class ResumeIncomplete(Redirection):
271 """Exception mapping a ``308 Resume Incomplete`` response.
272
273 .. note:: :attr:`http.client.PERMANENT_REDIRECT` is ``308``, but Google
274 APIs differ in their use of this status code.
275 """
276
277 code = 308
278
279
280class ClientError(GoogleAPICallError):
281 """Base class for all client error (HTTP 4xx) responses."""
282
283
284class BadRequest(ClientError):
285 """Exception mapping a ``400 Bad Request`` response."""
286
287 code = http.client.BAD_REQUEST
288
289
290class InvalidArgument(BadRequest):
291 """Exception mapping a :attr:`grpc.StatusCode.INVALID_ARGUMENT` error."""
292
293 grpc_status_code = grpc.StatusCode.INVALID_ARGUMENT if grpc is not None else None
294
295
296class FailedPrecondition(BadRequest):
297 """Exception mapping a :attr:`grpc.StatusCode.FAILED_PRECONDITION`
298 error."""
299
300 grpc_status_code = grpc.StatusCode.FAILED_PRECONDITION if grpc is not None else None
301
302
303class OutOfRange(BadRequest):
304 """Exception mapping a :attr:`grpc.StatusCode.OUT_OF_RANGE` error."""
305
306 grpc_status_code = grpc.StatusCode.OUT_OF_RANGE if grpc is not None else None
307
308
309class Unauthorized(ClientError):
310 """Exception mapping a ``401 Unauthorized`` response."""
311
312 code = http.client.UNAUTHORIZED
313
314
315class Unauthenticated(Unauthorized):
316 """Exception mapping a :attr:`grpc.StatusCode.UNAUTHENTICATED` error."""
317
318 grpc_status_code = grpc.StatusCode.UNAUTHENTICATED if grpc is not None else None
319
320
321class Forbidden(ClientError):
322 """Exception mapping a ``403 Forbidden`` response."""
323
324 code = http.client.FORBIDDEN
325
326
327class PermissionDenied(Forbidden):
328 """Exception mapping a :attr:`grpc.StatusCode.PERMISSION_DENIED` error."""
329
330 grpc_status_code = grpc.StatusCode.PERMISSION_DENIED if grpc is not None else None
331
332
333class NotFound(ClientError):
334 """Exception mapping a ``404 Not Found`` response or a
335 :attr:`grpc.StatusCode.NOT_FOUND` error."""
336
337 code = http.client.NOT_FOUND
338 grpc_status_code = grpc.StatusCode.NOT_FOUND if grpc is not None else None
339
340
341class MethodNotAllowed(ClientError):
342 """Exception mapping a ``405 Method Not Allowed`` response."""
343
344 code = http.client.METHOD_NOT_ALLOWED
345
346
347class Conflict(ClientError):
348 """Exception mapping a ``409 Conflict`` response."""
349
350 code = http.client.CONFLICT
351
352
353class AlreadyExists(Conflict):
354 """Exception mapping a :attr:`grpc.StatusCode.ALREADY_EXISTS` error."""
355
356 grpc_status_code = grpc.StatusCode.ALREADY_EXISTS if grpc is not None else None
357
358
359class Aborted(Conflict):
360 """Exception mapping a :attr:`grpc.StatusCode.ABORTED` error."""
361
362 grpc_status_code = grpc.StatusCode.ABORTED if grpc is not None else None
363
364
365class LengthRequired(ClientError):
366 """Exception mapping a ``411 Length Required`` response."""
367
368 code = http.client.LENGTH_REQUIRED
369
370
371class PreconditionFailed(ClientError):
372 """Exception mapping a ``412 Precondition Failed`` response."""
373
374 code = http.client.PRECONDITION_FAILED
375
376
377class RequestRangeNotSatisfiable(ClientError):
378 """Exception mapping a ``416 Request Range Not Satisfiable`` response."""
379
380 code = http.client.REQUESTED_RANGE_NOT_SATISFIABLE
381
382
383class TooManyRequests(ClientError):
384 """Exception mapping a ``429 Too Many Requests`` response."""
385
386 code = http.client.TOO_MANY_REQUESTS
387
388
389class ResourceExhausted(TooManyRequests):
390 """Exception mapping a :attr:`grpc.StatusCode.RESOURCE_EXHAUSTED` error."""
391
392 grpc_status_code = grpc.StatusCode.RESOURCE_EXHAUSTED if grpc is not None else None
393
394
395class Cancelled(ClientError):
396 """Exception mapping a :attr:`grpc.StatusCode.CANCELLED` error."""
397
398 # This maps to HTTP status code 499. See
399 # https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
400 code = 499
401 grpc_status_code = grpc.StatusCode.CANCELLED if grpc is not None else None
402
403
404class ServerError(GoogleAPICallError):
405 """Base for 5xx responses."""
406
407
408class InternalServerError(ServerError):
409 """Exception mapping a ``500 Internal Server Error`` response. or a
410 :attr:`grpc.StatusCode.INTERNAL` error."""
411
412 code = http.client.INTERNAL_SERVER_ERROR
413 grpc_status_code = grpc.StatusCode.INTERNAL if grpc is not None else None
414
415
416class Unknown(ServerError):
417 """Exception mapping a :attr:`grpc.StatusCode.UNKNOWN` error."""
418
419 grpc_status_code = grpc.StatusCode.UNKNOWN if grpc is not None else None
420
421
422class DataLoss(ServerError):
423 """Exception mapping a :attr:`grpc.StatusCode.DATA_LOSS` error."""
424
425 grpc_status_code = grpc.StatusCode.DATA_LOSS if grpc is not None else None
426
427
428class MethodNotImplemented(ServerError):
429 """Exception mapping a ``501 Not Implemented`` response or a
430 :attr:`grpc.StatusCode.UNIMPLEMENTED` error."""
431
432 code = http.client.NOT_IMPLEMENTED
433 grpc_status_code = grpc.StatusCode.UNIMPLEMENTED if grpc is not None else None
434
435
436class BadGateway(ServerError):
437 """Exception mapping a ``502 Bad Gateway`` response."""
438
439 code = http.client.BAD_GATEWAY
440
441
442class ServiceUnavailable(ServerError):
443 """Exception mapping a ``503 Service Unavailable`` response or a
444 :attr:`grpc.StatusCode.UNAVAILABLE` error."""
445
446 code = http.client.SERVICE_UNAVAILABLE
447 grpc_status_code = grpc.StatusCode.UNAVAILABLE if grpc is not None else None
448
449
450class GatewayTimeout(ServerError):
451 """Exception mapping a ``504 Gateway Timeout`` response."""
452
453 code = http.client.GATEWAY_TIMEOUT
454
455
456class DeadlineExceeded(GatewayTimeout):
457 """Exception mapping a :attr:`grpc.StatusCode.DEADLINE_EXCEEDED` error."""
458
459 grpc_status_code = grpc.StatusCode.DEADLINE_EXCEEDED if grpc is not None else None
460
461
462class AsyncRestUnsupportedParameterError(NotImplementedError):
463 """Raised when an unsupported parameter is configured against async rest transport."""
464
465 pass
466
467
468class ResumableTransferError(GoogleAPICallError):
469 """Base class for resumable transfer errors."""
470
471 upload_url: Optional[str] = None
472 chunk_size: Optional[int] = None
473
474 def __init__(
475 self,
476 message: str,
477 *args,
478 upload_url: Optional[str] = None,
479 chunk_size: Optional[int] = None,
480 **kwargs,
481 ) -> None:
482 super().__init__(message, *args, **kwargs)
483 self.upload_url = upload_url
484 self.chunk_size = chunk_size
485
486
487class TransferStalledError(ResumableTransferError):
488 """Raised when upload throughput stays below minimum rate past stall timeout."""
489
490
491class UnseekableStreamError(ResumableTransferError):
492 """Raised when server recovery requires rewinding a non-seekable stream."""
493
494
495class UploadCancelledError(ResumableTransferError):
496 """Raised when the upload is cancelled by the client or server."""
497
498
499class MissingStatusHeaderError(ResumableTransferError):
500 """Raised when server response lacks the required X-Goog-Upload-Status header."""
501
502
503def exception_class_for_http_status(status_code):
504 """Return the exception class for a specific HTTP status code.
505
506 Args:
507 status_code (int): The HTTP status code.
508
509 Returns:
510 :func:`type`: the appropriate subclass of :class:`GoogleAPICallError`.
511 """
512 return _HTTP_CODE_TO_EXCEPTION.get(status_code, GoogleAPICallError)
513
514
515def from_http_status(status_code, message, **kwargs):
516 """Create a :class:`GoogleAPICallError` from an HTTP status code.
517
518 Args:
519 status_code (int): The HTTP status code.
520 message (str): The exception message.
521 kwargs: Additional arguments passed to the :class:`GoogleAPICallError`
522 constructor.
523
524 Returns:
525 GoogleAPICallError: An instance of the appropriate subclass of
526 :class:`GoogleAPICallError`.
527 """
528 error_class = exception_class_for_http_status(status_code)
529 error = error_class(message, **kwargs)
530
531 if error.code is None:
532 error.code = status_code
533
534 return error
535
536
537def _format_rest_error_message(error, method, url):
538 method = method.upper() if method else None
539 message = "{method} {url}: {error}".format(
540 method=method,
541 url=url,
542 error=error,
543 )
544 return message
545
546
547# NOTE: We're moving away from `from_http_status` because it expects an aiohttp response compared
548# to `format_http_response_error` which expects a more abstract response from google.auth and is
549# compatible with both sync and async response types.
550# TODO(https://github.com/googleapis/python-api-core/issues/691): Add type hint for response.
551def format_http_response_error(
552 response, method: str, url: str, payload: Optional[Dict] = None
553):
554 """Create a :class:`GoogleAPICallError` from a google auth rest response.
555
556 Args:
557 response Union[google.auth.transport.Response, google.auth.aio.transport.Response]: The HTTP response.
558 method Optional(str): The HTTP request method.
559 url Optional(str): The HTTP request url.
560 payload Optional(dict): The HTTP response payload. If not passed in, it is read from response for a response type of google.auth.transport.Response.
561
562 Returns:
563 GoogleAPICallError: An instance of the appropriate subclass of
564 :class:`GoogleAPICallError`, with the message and errors populated
565 from the response.
566 """
567 if isinstance(payload, list):
568 payload = next((item for item in payload if isinstance(item, dict)), {})
569 payload = {} if not payload else payload
570 error_message = payload.get("error", {}).get("message", "unknown error")
571 errors = payload.get("error", {}).get("errors", ())
572 # In JSON, details are already formatted in developer-friendly way.
573 details = payload.get("error", {}).get("details", ())
574 error_info_list = list(
575 filter(
576 lambda detail: detail.get("@type", "")
577 == "type.googleapis.com/google.rpc.ErrorInfo",
578 details,
579 )
580 )
581 error_info = error_info_list[0] if error_info_list else None
582 message = _format_rest_error_message(error_message, method, url)
583
584 exception = from_http_status(
585 response.status_code,
586 message,
587 errors=errors,
588 details=details,
589 response=response,
590 error_info=error_info,
591 )
592 return exception
593
594
595def from_http_response(response):
596 """Create a :class:`GoogleAPICallError` from a :class:`requests.Response`.
597
598 Args:
599 response (requests.Response): The HTTP response.
600
601 Returns:
602 GoogleAPICallError: An instance of the appropriate subclass of
603 :class:`GoogleAPICallError`, with the message and errors populated
604 from the response.
605 """
606 try:
607 payload = response.json()
608 except ValueError:
609 payload = {"error": {"message": response.text or "unknown error"}}
610 return format_http_response_error(
611 response, response.request.method, response.request.url, payload
612 )
613
614
615def exception_class_for_grpc_status(status_code):
616 """Return the exception class for a specific :class:`grpc.StatusCode`.
617
618 Args:
619 status_code (grpc.StatusCode): The gRPC status code.
620
621 Returns:
622 :func:`type`: the appropriate subclass of :class:`GoogleAPICallError`.
623 """
624 return _GRPC_CODE_TO_EXCEPTION.get(status_code, GoogleAPICallError)
625
626
627def from_grpc_status(status_code, message, **kwargs):
628 """Create a :class:`GoogleAPICallError` from a :class:`grpc.StatusCode`.
629
630 Args:
631 status_code (Union[grpc.StatusCode, int]): The gRPC status code.
632 message (str): The exception message.
633 kwargs: Additional arguments passed to the :class:`GoogleAPICallError`
634 constructor.
635
636 Returns:
637 GoogleAPICallError: An instance of the appropriate subclass of
638 :class:`GoogleAPICallError`.
639 """
640
641 if isinstance(status_code, int):
642 status_code = _INT_TO_GRPC_CODE.get(status_code, status_code)
643
644 error_class = exception_class_for_grpc_status(status_code)
645 error = error_class(message, **kwargs)
646
647 if error.grpc_status_code is None:
648 error.grpc_status_code = status_code
649
650 return error
651
652
653def _extract_status_code(exc: BaseException | None, default: str | None = None) -> str:
654 """Extract canonical status code name string from an exception.
655
656 Status code name strings are resolved by inspecting the following:
657 * Chained exceptions: Unwraps RetryError or __cause__ to the root error.
658 * Enum & code attributes: Inspects .grpc_status_code on GoogleAPICallError
659 or .code on gRPC/HTTP errors.
660 * Integer status codes: Maps raw gRPC integer status codes or HTTP status
661 codes to canonical enum names.
662 * Fallback: Uses ``default`` if provided, otherwise defaults to the
663 exception class name.
664
665 Args:
666 exc (BaseException | None): The exception to extract the status code
667 name from.
668 default (str | None): Optional fallback status code string when
669 ``exc`` carries no status code.
670
671 Returns:
672 str: The canonical status code name (e.g. "NOT_FOUND", "UNAVAILABLE",
673 "BAD_REQUEST") or class name.
674 """
675 if exc is None:
676 return default or ""
677
678 # 1. Unwrap chained exceptions: unwrap RetryError or __cause__ to the root failure
679 target = getattr(exc, "cause", None) or getattr(exc, "__cause__", None) or exc
680
681 # 2. Check enum & code attributes: .grpc_status_code enum or callable/non-callable .code
682 status = getattr(target, "grpc_status_code", None)
683 if status is None and hasattr(target, "code"):
684 try:
685 status = target.code() if callable(target.code) else target.code
686 # Defensive fallback: observability status extraction must never raise
687 # if a third-party or mock exception implements a failing .code() method.
688 except Exception:
689 status = None
690
691 name = getattr(status, "name", None)
692 if name:
693 return str(name)
694
695 # 3. Check integer status codes: map raw gRPC integer codes or HTTP status codes to canonical names
696 if isinstance(status, int):
697 if status in _INT_TO_GRPC_CODE:
698 grpc_code = _INT_TO_GRPC_CODE[status]
699 return getattr(grpc_code, "name", str(grpc_code))
700 try:
701 return http.HTTPStatus(status).name
702 except ValueError:
703 return str(status)
704
705 # 4. Fallback: default to explicit default or exception class name for standard Python errors
706 return default or target.__class__.__name__
707
708
709def _is_informative_grpc_error(rpc_exc):
710 return hasattr(rpc_exc, "code") and hasattr(rpc_exc, "details")
711
712
713def _parse_grpc_error_details(rpc_exc):
714 if not rpc_status: # pragma: NO COVER
715 _warn_could_not_import_grpcio_status()
716 return [], None
717 try:
718 status = rpc_status.from_call(rpc_exc)
719 except NotImplementedError: # workaround
720 return [], None
721
722 if not status:
723 return [], None
724
725 possible_errors = [
726 error_details_pb2.BadRequest,
727 error_details_pb2.PreconditionFailure,
728 error_details_pb2.QuotaFailure,
729 error_details_pb2.ErrorInfo,
730 error_details_pb2.RetryInfo,
731 error_details_pb2.ResourceInfo,
732 error_details_pb2.RequestInfo,
733 error_details_pb2.DebugInfo,
734 error_details_pb2.Help,
735 error_details_pb2.LocalizedMessage,
736 ]
737 error_info = None
738 error_details = []
739 for detail in status.details:
740 matched_detail_cls = list(
741 filter(lambda x: detail.Is(x.DESCRIPTOR), possible_errors)
742 )
743 # If nothing matched, use detail directly.
744 if len(matched_detail_cls) == 0:
745 info = detail
746 else:
747 info = matched_detail_cls[0]()
748 detail.Unpack(info)
749 error_details.append(info)
750 if isinstance(info, error_details_pb2.ErrorInfo):
751 error_info = info
752 return error_details, error_info
753
754
755def from_grpc_error(rpc_exc):
756 """Create a :class:`GoogleAPICallError` from a :class:`grpc.RpcError`.
757
758 Args:
759 rpc_exc (grpc.RpcError): The gRPC error.
760
761 Returns:
762 GoogleAPICallError: An instance of the appropriate subclass of
763 :class:`GoogleAPICallError`.
764 """
765 # NOTE(lidiz) All gRPC error shares the parent class grpc.RpcError.
766 # However, check for grpc.RpcError breaks backward compatibility.
767 if (
768 grpc is not None and isinstance(rpc_exc, grpc.Call)
769 ) or _is_informative_grpc_error(rpc_exc):
770 details, err_info = _parse_grpc_error_details(rpc_exc)
771 message = rpc_exc.details()
772 if (
773 grpc is not None
774 and rpc_exc.code() == grpc.StatusCode.UNIMPLEMENTED
775 and "Received http2 header with status: 404" in message
776 ):
777 message = (
778 f"{message}. This usually indicates that the 'api_endpoint' "
779 "configuration in ClientOptions is incorrect, contains a typo, "
780 "or is an invalid regional endpoint for this service."
781 )
782 return from_grpc_status(
783 rpc_exc.code(),
784 message,
785 errors=(rpc_exc,),
786 details=details,
787 response=rpc_exc,
788 error_info=err_info,
789 )
790 else:
791 return GoogleAPICallError(str(rpc_exc), errors=(rpc_exc,), response=rpc_exc)