25
25
from typing import Dict
26
26
from typing import Union
27
27
28
+ from google .rpc import error_details_pb2
29
+
28
30
try :
29
31
import grpc
32
+ from grpc_status import rpc_status
30
33
except ImportError : # pragma: NO COVER
31
34
grpc = None
35
+ rpc_status = None
32
36
33
37
# Lookup tables for mapping exceptions from HTTP and gRPC transports.
34
38
# Populated by _GoogleAPICallErrorMeta
@@ -97,6 +101,7 @@ class GoogleAPICallError(GoogleAPIError, metaclass=_GoogleAPICallErrorMeta):
97
101
Args:
98
102
message (str): The exception message.
99
103
errors (Sequence[Any]): An optional list of error details.
104
+ details (Sequence[Any]): An optional list of objects defined in google.rpc.error_details.
100
105
response (Union[requests.Request, grpc.Call]): The response or
101
106
gRPC call metadata.
102
107
"""
@@ -117,15 +122,19 @@ class GoogleAPICallError(GoogleAPIError, metaclass=_GoogleAPICallErrorMeta):
117
122
This may be ``None`` if the exception does not match up to a gRPC error.
118
123
"""
119
124
120
- def __init__ (self , message , errors = (), response = None ):
125
+ def __init__ (self , message , errors = (), details = (), response = None ):
121
126
super (GoogleAPICallError , self ).__init__ (message )
122
127
self .message = message
123
128
"""str: The exception message."""
124
129
self ._errors = errors
130
+ self ._details = details
125
131
self ._response = response
126
132
127
133
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 )
129
138
130
139
@property
131
140
def errors (self ):
@@ -136,6 +145,19 @@ def errors(self):
136
145
"""
137
146
return list (self ._errors )
138
147
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
+
139
161
@property
140
162
def response (self ):
141
163
"""Optional[Union[requests.Request, grpc.Call]]: The response or
@@ -409,13 +431,15 @@ def from_http_response(response):
409
431
410
432
error_message = payload .get ("error" , {}).get ("message" , "unknown error" )
411
433
errors = payload .get ("error" , {}).get ("errors" , ())
434
+ # In JSON, details are already formatted in developer-friendly way.
435
+ details = payload .get ("error" , {}).get ("details" , ())
412
436
413
437
message = "{method} {url}: {error}" .format (
414
438
method = response .request .method , url = response .request .url , error = error_message
415
439
)
416
440
417
441
exception = from_http_status (
418
- response .status_code , message , errors = errors , response = response
442
+ response .status_code , message , errors = errors , details = details , response = response
419
443
)
420
444
return exception
421
445
@@ -462,6 +486,37 @@ def _is_informative_grpc_error(rpc_exc):
462
486
return hasattr (rpc_exc , "code" ) and hasattr (rpc_exc , "details" )
463
487
464
488
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
+
465
520
def from_grpc_error (rpc_exc ):
466
521
"""Create a :class:`GoogleAPICallError` from a :class:`grpc.RpcError`.
467
522
@@ -476,7 +531,11 @@ def from_grpc_error(rpc_exc):
476
531
# However, check for grpc.RpcError breaks backward compatibility.
477
532
if isinstance (rpc_exc , grpc .Call ) or _is_informative_grpc_error (rpc_exc ):
478
533
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 ,
480
539
)
481
540
else :
482
541
return GoogleAPICallError (str (rpc_exc ), errors = (rpc_exc ,), response = rpc_exc )
0 commit comments