-
Notifications
You must be signed in to change notification settings - Fork 90
feat: Add error_details property to GoogleAPICallError based on google.rpc.status.details. #286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
288019c
04b5775
5cebe8e
5b3f7c1
7b7504d
fe708ae
813fc33
d10157f
88e6ffa
b6c9e56
02b082a
12edc5a
626f7de
a844fed
a830887
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,12 +22,15 @@ | |
from __future__ import unicode_literals | ||
|
||
import http.client | ||
from google.rpc import error_details_pb2 | ||
|
||
try: | ||
import grpc | ||
from grpc_status import rpc_status | ||
tseaver marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
except ImportError: # pragma: NO COVER | ||
grpc = None | ||
rpc_status = None | ||
|
||
# Lookup tables for mapping exceptions from HTTP and gRPC transports. | ||
# Populated by _GoogleAPICallErrorMeta | ||
|
@@ -96,6 +99,7 @@ class GoogleAPICallError(GoogleAPIError, metaclass=_GoogleAPICallErrorMeta): | |
Args: | ||
message (str): The exception message. | ||
errors (Sequence[Any]): An optional list of error details. | ||
details (Sequence[Any]): An optional list of objects defined in google.rpc.error_details. | ||
response (Union[requests.Request, grpc.Call]): The response or | ||
gRPC call metadata. | ||
""" | ||
|
@@ -116,15 +120,19 @@ class GoogleAPICallError(GoogleAPIError, metaclass=_GoogleAPICallErrorMeta): | |
This may be ``None`` if the exception does not match up to a gRPC error. | ||
""" | ||
|
||
def __init__(self, message, errors=(), response=None): | ||
def __init__(self, message, errors=(), details=(), response=None): | ||
tseaver marked this conversation as resolved.
Show resolved
Hide resolved
|
||
super(GoogleAPICallError, self).__init__(message) | ||
self.message = message | ||
"""str: The exception message.""" | ||
self._errors = errors | ||
self._details = details | ||
self._response = response | ||
|
||
def __str__(self): | ||
return "{} {}".format(self.code, self.message) | ||
if self.error_details: | ||
return "{} {} {}".format(self.code, self.message, self.error_details) | ||
else: | ||
return "{} {}".format(self.code, self.message) | ||
|
||
@property | ||
def errors(self): | ||
|
@@ -135,6 +143,19 @@ def errors(self): | |
""" | ||
return list(self._errors) | ||
|
||
@property | ||
def error_details(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I followed the naming convention in design doc. I personally prefer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed the name to "details". |
||
"""Information contained in google.rpc.status.details. | ||
|
||
Reference: | ||
https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto | ||
https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto | ||
|
||
Returns: | ||
Sequence[Any]: A list of structured objects from error_details.proto | ||
""" | ||
return list(self._details) | ||
|
||
@property | ||
def response(self): | ||
"""Optional[Union[requests.Request, grpc.Call]]: The response or | ||
|
@@ -408,13 +429,15 @@ def from_http_response(response): | |
|
||
error_message = payload.get("error", {}).get("message", "unknown error") | ||
errors = payload.get("error", {}).get("errors", ()) | ||
# In JSON, details are already formatted in developer-friendly way. | ||
details = payload.get("error", {}).get("details", ()) | ||
|
||
message = "{method} {url}: {error}".format( | ||
method=response.request.method, url=response.request.url, error=error_message | ||
) | ||
|
||
exception = from_http_status( | ||
response.status_code, message, errors=errors, response=response | ||
response.status_code, message, errors=errors, details=details, response=response | ||
) | ||
return exception | ||
|
||
|
@@ -461,6 +484,37 @@ def _is_informative_grpc_error(rpc_exc): | |
return hasattr(rpc_exc, "code") and hasattr(rpc_exc, "details") | ||
|
||
|
||
def _parse_grpc_error_details(rpc_exc): | ||
status = rpc_status.from_call(rpc_exc) | ||
if not status: | ||
return [] | ||
possible_errors = [ | ||
error_details_pb2.BadRequest, | ||
error_details_pb2.PreconditionFailure, | ||
error_details_pb2.QuotaFailure, | ||
error_details_pb2.ErrorInfo, | ||
error_details_pb2.RetryInfo, | ||
error_details_pb2.ResourceInfo, | ||
error_details_pb2.RequestInfo, | ||
error_details_pb2.DebugInfo, | ||
error_details_pb2.Help, | ||
error_details_pb2.LocalizedMessage, | ||
] | ||
tseaver marked this conversation as resolved.
Show resolved
Hide resolved
|
||
error_details = [] | ||
for detail in status.details: | ||
matched_detail_cls = list( | ||
filter(lambda x: detail.Is(x.DESCRIPTOR), possible_errors) | ||
) | ||
# If nothing matched, use detail directly. | ||
if len(matched_detail_cls) == 0: | ||
info = detail | ||
tseaver marked this conversation as resolved.
Show resolved
Hide resolved
|
||
else: | ||
info = matched_detail_cls[0]() | ||
detail.Unpack(info) | ||
error_details.append(info) | ||
return error_details | ||
|
||
|
||
def from_grpc_error(rpc_exc): | ||
"""Create a :class:`GoogleAPICallError` from a :class:`grpc.RpcError`. | ||
|
||
|
@@ -475,7 +529,11 @@ def from_grpc_error(rpc_exc): | |
# However, check for grpc.RpcError breaks backward compatibility. | ||
if isinstance(rpc_exc, grpc.Call) or _is_informative_grpc_error(rpc_exc): | ||
return from_grpc_status( | ||
rpc_exc.code(), rpc_exc.details(), errors=(rpc_exc,), response=rpc_exc | ||
rpc_exc.code(), | ||
rpc_exc.details(), | ||
errors=(rpc_exc,), | ||
details=_parse_grpc_error_details(rpc_exc), | ||
response=rpc_exc, | ||
) | ||
else: | ||
return GoogleAPICallError(str(rpc_exc), errors=(rpc_exc,), response=rpc_exc) |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -5,7 +5,7 @@ | |||||
# | ||||||
# e.g., if setup.py has "foo >= 1.14.0, < 2.0.0dev", | ||||||
# Then this file should have foo==1.14.0 | ||||||
googleapis-common-protos==1.6.0 | ||||||
googleapis-common-protos==1.52.0 | ||||||
protobuf==3.12.0 | ||||||
google-auth==1.25.0 | ||||||
requests==2.18.0 | ||||||
|
@@ -14,3 +14,4 @@ packaging==14.3 | |||||
grpcio==1.33.2 | ||||||
grpcio-gcp==0.2.2 | ||||||
grpcio-gcp==0.2.2 | ||||||
grpcio-status==1.33.2 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
(to match the lower bound in setup.py) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a bit confusing. I initially put 1.0.0 lower bound in setup.py without giving it too much thought. As we found out later, grpcio-status requires the same version as grpc (see]). To avoid confusion, I say we should change lower bound in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed it to 1.33.2 |
Uh oh!
There was an error while loading. Please reload this page.