-
Notifications
You must be signed in to change notification settings - Fork 340
Migrating FCM Send APIs to the New Exceptions #297
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 8 commits
3218695
e734cf9
8df345a
ce4a789
3b6b78d
20d2e87
c0cf6f7
0ea0f82
130a42f
de29a6b
c536f5b
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 |
---|---|---|
|
@@ -14,13 +14,19 @@ | |
|
||
"""Internal utilities common to all modules.""" | ||
|
||
import json | ||
import socket | ||
|
||
import googleapiclient | ||
import httplib2 | ||
import requests | ||
import six | ||
|
||
import firebase_admin | ||
from firebase_admin import exceptions | ||
|
||
|
||
_STATUS_TO_EXCEPTION_TYPE = { | ||
_ERROR_CODE_TO_EXCEPTION_TYPE = { | ||
400: exceptions.InvalidArgumentError, | ||
401: exceptions.UnauthenticatedError, | ||
403: exceptions.PermissionDeniedError, | ||
|
@@ -29,6 +35,34 @@ | |
429: exceptions.ResourceExhaustedError, | ||
500: exceptions.InternalError, | ||
503: exceptions.UnavailableError, | ||
|
||
exceptions.INVALID_ARGUMENT: exceptions.InvalidArgumentError, | ||
exceptions.FAILED_PRECONDITION: exceptions.FailedPreconditionError, | ||
exceptions.OUT_OF_RANGE: exceptions.OutOfRangeError, | ||
exceptions.UNAUTHENTICATED: exceptions.UnauthenticatedError, | ||
exceptions.PERMISSION_DENIED: exceptions.PermissionDeniedError, | ||
exceptions.NOT_FOUND: exceptions.NotFoundError, | ||
exceptions.ABORTED: exceptions.AbortedError, | ||
exceptions.ALREADY_EXISTS: exceptions.AlreadyExistsError, | ||
exceptions.RESOURCE_EXHAUSTED: exceptions.ResourceExhaustedError, | ||
exceptions.CANCELLED: exceptions.CancelledError, | ||
exceptions.DATA_LOSS: exceptions.DataLossError, | ||
exceptions.UNKNOWN: exceptions.UnknownError, | ||
exceptions.INTERNAL: exceptions.InternalError, | ||
exceptions.UNAVAILABLE: exceptions.UnavailableError, | ||
exceptions.DEADLINE_EXCEEDED: exceptions.DeadlineExceededError, | ||
} | ||
|
||
|
||
_HTTP_STATUS_TO_ERROR_CODE = { | ||
400: exceptions.INVALID_ARGUMENT, | ||
401: exceptions.UNAUTHENTICATED, | ||
403: exceptions.PERMISSION_DENIED, | ||
404: exceptions.NOT_FOUND, | ||
409: exceptions.CONFLICT, | ||
429: exceptions.RESOURCE_EXHAUSTED, | ||
500: exceptions.INTERNAL, | ||
503: exceptions.UNAVAILABLE, | ||
} | ||
|
||
|
||
|
@@ -45,19 +79,50 @@ def _get_initialized_app(app): | |
raise ValueError('Illegal app argument. Argument must be of type ' | ||
' firebase_admin.App, but given "{0}".'.format(type(app))) | ||
|
||
|
||
def get_app_service(app, name, initializer): | ||
app = _get_initialized_app(app) | ||
return app._get_service(name, initializer) # pylint: disable=protected-access | ||
|
||
def handle_requests_error(error, message=None, status=None): | ||
|
||
def handle_platform_error_from_requests(error, handle_func=None): | ||
"""Constructs a ``FirebaseError`` from the given requests error. | ||
|
||
This can be used to handle errors returned by Google Cloud Platform (GCP) APIs. | ||
|
||
Args: | ||
error: An error raised by the reqests module while making an HTTP call to a GCP API. | ||
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. s/reqests/requests/ 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. Done |
||
handle_func: A function that can be used to handle platform errors in a custom way. When | ||
specified, this function will be called with four arguments -- parsed error response, | ||
error message, source exception from requests and the HTTP response object. | ||
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. Personally, I'd prefer an oxford comma in there. :-) But more seriously, please make this a bulleted list so that it's easier to scan what the four arguments are. |
||
|
||
Returns: | ||
FirebaseError: A ``FirebaseError`` that can be raised to the user code. | ||
""" | ||
if error.response is None: | ||
return handle_requests_error(error) | ||
|
||
response = error.response | ||
content = response.content.decode() | ||
status_code = response.status_code | ||
error_dict, code, message = _parse_platform_error(content, status_code) | ||
exc = None | ||
if handle_func: | ||
exc = handle_func(error_dict, message, error, error.response) | ||
|
||
return exc if exc else handle_requests_error(error, message, code) | ||
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. If might be a little cleaner than all this if you can make
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've sort of done both, but not quite the way described in your comment. PTAL. |
||
|
||
|
||
def handle_requests_error(error, message=None, code=None): | ||
"""Constructs a ``FirebaseError`` from the given requests error. | ||
|
||
Args: | ||
error: An error raised by the reqests module while making an HTTP call. | ||
message: A message to be included in the resulting ``FirebaseError`` (optional). If not | ||
specified the string representation of the ``error`` argument is used as the message. | ||
status: An HTTP status code that will be used to determine the resulting error type | ||
(optional). If not specified the HTTP status code on the error response is used. | ||
code: An HTTP status code or GCP error code that will be used to determine the resulting | ||
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. Yeah, it still seems sketchy to me to mix types here. Would it be possible to make this one or the other? It occurs to me that I'm not sure if that's a good idea, but it's worth thinking about. 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.
But I do still need some branching since we allow |
||
error type (optional). If not specified the HTTP status code on the error response is | ||
used. | ||
|
||
Returns: | ||
FirebaseError: A ``FirebaseError`` that can be raised to the user code. | ||
|
@@ -75,9 +140,119 @@ def handle_requests_error(error, message=None, status=None): | |
message='Unknown error while making a remote service call: {0}'.format(error), | ||
cause=error) | ||
|
||
if not status: | ||
status = error.response.status_code | ||
if not code: | ||
code = error.response.status_code | ||
if not message: | ||
message = str(error) | ||
err_type = _STATUS_TO_EXCEPTION_TYPE.get(status, exceptions.UnknownError) | ||
|
||
err_type = _lookup_error_type(code) | ||
return err_type(message=message, cause=error, http_response=error.response) | ||
|
||
|
||
def handle_platform_error_from_googleapiclient(error, handle_func=None): | ||
"""Constructs a ``FirebaseError`` from the given googleapiclient error. | ||
|
||
This can be used to handle errors returned by Google Cloud Platform (GCP) APIs. | ||
|
||
Args: | ||
error: An error raised by the googleapiclient while making an HTTP call to a GCP API. | ||
handle_func: A function that can be used to handle platform errors in a custom way. When | ||
specified, this function will be called with four arguments -- parsed error response, | ||
error message, source exception from googleapiclient and a HTTP response object. | ||
|
||
Returns: | ||
FirebaseError: A ``FirebaseError`` that can be raised to the user code. | ||
""" | ||
if not isinstance(error, googleapiclient.errors.HttpError): | ||
return handle_googleapiclient_error(error) | ||
|
||
content = error.content.decode() | ||
status_code = error.resp.status | ||
error_dict, code, message = _parse_platform_error(content, status_code) | ||
exc = None | ||
if handle_func: | ||
http_response = _http_response_from_googleapiclient_error(error) | ||
exc = handle_func(error_dict, message, error, http_response) | ||
|
||
return exc if exc else handle_googleapiclient_error(error, message, code) | ||
|
||
|
||
def handle_googleapiclient_error(error, message=None, code=None): | ||
"""Constructs a ``FirebaseError`` from the given googleapiclient error. | ||
|
||
Args: | ||
error: An error raised by the googleapiclient module while making an HTTP call. | ||
message: A message to be included in the resulting ``FirebaseError`` (optional). If not | ||
specified the string representation of the ``error`` argument is used as the message. | ||
code: An HTTP status code or GCP error code that will be used to determine the resulting | ||
error type (optional). If not specified the HTTP status code on the error response is | ||
used. | ||
|
||
Returns: | ||
FirebaseError: A ``FirebaseError`` that can be raised to the user code. | ||
""" | ||
if isinstance(error, socket.timeout) or ( | ||
isinstance(error, socket.error) and 'timed out' in str(error)): | ||
return exceptions.DeadlineExceededError( | ||
message='Timed out while making an API call: {0}'.format(error), | ||
cause=error) | ||
elif isinstance(error, httplib2.ServerNotFoundError): | ||
return exceptions.UnavailableError( | ||
message='Failed to establish a connection: {0}'.format(error), | ||
cause=error) | ||
elif not isinstance(error, googleapiclient.errors.HttpError): | ||
return exceptions.UnknownError( | ||
message='Unknown error while making a remote service call: {0}'.format(error), | ||
cause=error) | ||
|
||
if not code: | ||
code = error.resp.status | ||
if not message: | ||
message = str(error) | ||
|
||
http_response = _http_response_from_googleapiclient_error(error) | ||
err_type = _lookup_error_type(code) | ||
return err_type(message=message, cause=error, http_response=http_response) | ||
|
||
|
||
def _http_response_from_googleapiclient_error(error): | ||
"""Creates a requests HTTP Response object from the given googleapiclient error.""" | ||
resp = requests.models.Response() | ||
resp.raw = six.BytesIO(error.content) | ||
resp.status_code = error.resp.status | ||
return resp | ||
|
||
|
||
def _lookup_error_type(code): | ||
"""Maps an error code to an exception type.""" | ||
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. Please document what 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. Done |
||
return _ERROR_CODE_TO_EXCEPTION_TYPE.get(code, exceptions.UnknownError) | ||
|
||
|
||
def _parse_platform_error(content, status_code): | ||
"""Parses an HTTP error response from a Google Cloud Platform API and extracts the error code | ||
and message fields. | ||
|
||
Args: | ||
content: Decoded content of the response body. | ||
status_code: HTTP status code. | ||
|
||
Returns: | ||
tuple: A tuple containing error code and message. | ||
""" | ||
data = {} | ||
try: | ||
parsed_body = json.loads(content) | ||
if isinstance(parsed_body, dict): | ||
data = parsed_body | ||
except ValueError: | ||
pass | ||
|
||
error_dict = data.get('error', {}) | ||
code = error_dict.get('status') | ||
if not code: | ||
code = _HTTP_STATUS_TO_ERROR_CODE.get(status_code, exceptions.UNKNOWN) | ||
|
||
msg = error_dict.get('message') | ||
if not msg: | ||
msg = 'Unexpected HTTP response with status: {0}; body: {1}'.format(status_code, content) | ||
return error_dict, code, msg |
Uh oh!
There was an error while loading. Please reload this page.