2525from typing import Dict
2626from typing import Union
2727
28+ from google .rpc import error_details_pb2
29+
2830try :
2931 import grpc
32+ from grpc_status import rpc_status
3033except ImportError : # pragma: NO COVER
3134 grpc = None
35+ rpc_status = None
3236
3337# Lookup tables for mapping exceptions from HTTP and gRPC transports.
3438# Populated by _GoogleAPICallErrorMeta
@@ -97,6 +101,7 @@ class GoogleAPICallError(GoogleAPIError, metaclass=_GoogleAPICallErrorMeta):
97101 Args:
98102 message (str): The exception message.
99103 errors (Sequence[Any]): An optional list of error details.
104+ details (Sequence[Any]): An optional list of objects defined in google.rpc.error_details.
100105 response (Union[requests.Request, grpc.Call]): The response or
101106 gRPC call metadata.
102107 """
@@ -117,15 +122,19 @@ class GoogleAPICallError(GoogleAPIError, metaclass=_GoogleAPICallErrorMeta):
117122 This may be ``None`` if the exception does not match up to a gRPC error.
118123 """
119124
120- def __init__ (self , message , errors = (), response = None ):
125+ def __init__ (self , message , errors = (), details = (), response = None ):
121126 super (GoogleAPICallError , self ).__init__ (message )
122127 self .message = message
123128 """str: The exception message."""
124129 self ._errors = errors
130+ self ._details = details
125131 self ._response = response
126132
127133 def __str__ (self ):
128- return "{} {}" .format (self .code , self .message )
134+ if self .details :
135+ return "{} {} {}" .format (self .code , self .message , self .details )
136+ else :
137+ return "{} {}" .format (self .code , self .message )
129138
130139 @property
131140 def errors (self ):
@@ -136,6 +145,19 @@ def errors(self):
136145 """
137146 return list (self ._errors )
138147
148+ @property
149+ def details (self ):
150+ """Information contained in google.rpc.status.details.
151+
152+ Reference:
153+ https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto
154+ https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto
155+
156+ Returns:
157+ Sequence[Any]: A list of structured objects from error_details.proto
158+ """
159+ return list (self ._details )
160+
139161 @property
140162 def response (self ):
141163 """Optional[Union[requests.Request, grpc.Call]]: The response or
@@ -409,13 +431,15 @@ def from_http_response(response):
409431
410432 error_message = payload .get ("error" , {}).get ("message" , "unknown error" )
411433 errors = payload .get ("error" , {}).get ("errors" , ())
434+ # In JSON, details are already formatted in developer-friendly way.
435+ details = payload .get ("error" , {}).get ("details" , ())
412436
413437 message = "{method} {url}: {error}" .format (
414438 method = response .request .method , url = response .request .url , error = error_message
415439 )
416440
417441 exception = from_http_status (
418- response .status_code , message , errors = errors , response = response
442+ response .status_code , message , errors = errors , details = details , response = response
419443 )
420444 return exception
421445
@@ -462,6 +486,37 @@ def _is_informative_grpc_error(rpc_exc):
462486 return hasattr (rpc_exc , "code" ) and hasattr (rpc_exc , "details" )
463487
464488
489+ def _parse_grpc_error_details (rpc_exc ):
490+ status = rpc_status .from_call (rpc_exc )
491+ if not status :
492+ return []
493+ possible_errors = [
494+ error_details_pb2 .BadRequest ,
495+ error_details_pb2 .PreconditionFailure ,
496+ error_details_pb2 .QuotaFailure ,
497+ error_details_pb2 .ErrorInfo ,
498+ error_details_pb2 .RetryInfo ,
499+ error_details_pb2 .ResourceInfo ,
500+ error_details_pb2 .RequestInfo ,
501+ error_details_pb2 .DebugInfo ,
502+ error_details_pb2 .Help ,
503+ error_details_pb2 .LocalizedMessage ,
504+ ]
505+ error_details = []
506+ for detail in status .details :
507+ matched_detail_cls = list (
508+ filter (lambda x : detail .Is (x .DESCRIPTOR ), possible_errors )
509+ )
510+ # If nothing matched, use detail directly.
511+ if len (matched_detail_cls ) == 0 :
512+ info = detail
513+ else :
514+ info = matched_detail_cls [0 ]()
515+ detail .Unpack (info )
516+ error_details .append (info )
517+ return error_details
518+
519+
465520def from_grpc_error (rpc_exc ):
466521 """Create a :class:`GoogleAPICallError` from a :class:`grpc.RpcError`.
467522
@@ -476,7 +531,11 @@ def from_grpc_error(rpc_exc):
476531 # However, check for grpc.RpcError breaks backward compatibility.
477532 if isinstance (rpc_exc , grpc .Call ) or _is_informative_grpc_error (rpc_exc ):
478533 return from_grpc_status (
479- rpc_exc .code (), rpc_exc .details (), errors = (rpc_exc ,), response = rpc_exc
534+ rpc_exc .code (),
535+ rpc_exc .details (),
536+ errors = (rpc_exc ,),
537+ details = _parse_grpc_error_details (rpc_exc ),
538+ response = rpc_exc ,
480539 )
481540 else :
482541 return GoogleAPICallError (str (rpc_exc ), errors = (rpc_exc ,), response = rpc_exc )
0 commit comments