Skip to content

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

Merged
merged 11 commits into from
Jun 20, 2019
32 changes: 32 additions & 0 deletions firebase_admin/_messaging_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

import six

from firebase_admin import exceptions


class Message(object):
"""A message that can be sent via Firebase Cloud Messaging.
Expand Down Expand Up @@ -797,3 +799,33 @@ def default(self, obj): # pylint: disable=method-hidden
if target_count != 1:
raise ValueError('Exactly one of token, topic or condition must be specified.')
return result


class ThirdPartyAuthError(exceptions.UnauthenticatedError):
"""APNs certificate or web push auth key was invalid or missing."""

def __init__(self, message, cause=None, http_response=None):
exceptions.UnauthenticatedError.__init__(self, message, cause, http_response)


class QuotaExceededError(exceptions.ResourceExhaustedError):
"""Sending limit exceeded for the message target."""

def __init__(self, message, cause=None, http_response=None):
exceptions.ResourceExhaustedError.__init__(self, message, cause, http_response)


class SenderIdMismatchError(exceptions.PermissionDeniedError):
"""The authenticated sender ID is different from the sender ID for the registration token."""

def __init__(self, message, cause=None, http_response=None):
exceptions.PermissionDeniedError.__init__(self, message, cause, http_response)


class UnregisteredError(exceptions.NotFoundError):
"""App instance was unregistered from FCM.

This usually means that the token used is no longer valid and a new one must be used."""

def __init__(self, message, cause=None, http_response=None):
exceptions.NotFoundError.__init__(self, message, cause, http_response)
242 changes: 226 additions & 16 deletions firebase_admin/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,47 @@

"""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 = {
400: exceptions.InvalidArgumentError,
401: exceptions.UnauthenticatedError,
403: exceptions.PermissionDeniedError,
404: exceptions.NotFoundError,
409: exceptions.ConflictError,
429: exceptions.ResourceExhaustedError,
500: exceptions.InternalError,
503: exceptions.UnavailableError,
_ERROR_CODE_TO_EXCEPTION_TYPE = {
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.CONFLICT: exceptions.ConflictError,
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,
}


Expand All @@ -45,19 +71,69 @@ 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 requests module 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 three arguments. It has the same
signature as ```_handle_func_requests``, but may return ``None``.

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, message = _parse_platform_error(content, status_code)
exc = None
if handle_func:
exc = handle_func(error, message, error_dict)

return exc if exc else _handle_func_requests(error, message, error_dict)


def _handle_func_requests(error, message, error_dict):
"""Constructs a ``FirebaseError`` from the given GCP error.

Args:
error: An error raised by the requests module while making an HTTP call.
message: A message to be included in the resulting ``FirebaseError``.
error_dict: Parsed GCP error response.

Returns:
FirebaseError: A ``FirebaseError`` that can be raised to the user code or None.
"""
code = error_dict.get('status')
return handle_requests_error(error, message, code)


def handle_requests_error(error, message=None, code=None):
"""Constructs a ``FirebaseError`` from the given requests error.

This method is agnostic of the remote service that produced the error, whether it is a GCP
service or otherwise. Therefore, this method does not attempt to parse the error response in
any way.

Args:
error: An error raised by the reqests module while making an HTTP call.
error: An error raised by the requests 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: A 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 to determine a
suitable error code.

Returns:
FirebaseError: A ``FirebaseError`` that can be raised to the user code.
Expand All @@ -75,9 +151,143 @@ 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 = _http_status_to_error_code(error.response.status_code)
if not message:
message = str(error)
err_type = _STATUS_TO_EXCEPTION_TYPE.get(status, exceptions.UnknownError)

err_type = _error_code_to_exception_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 three arguments. It has the same
signature as ```_handle_func_googleapiclient``, but may return ``None``.

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, message = _parse_platform_error(content, status_code)
http_response = _http_response_from_googleapiclient_error(error)
exc = None
if handle_func:
exc = handle_func(error, message, error_dict, http_response)

return exc if exc else _handle_func_googleapiclient(error, message, error_dict, http_response)


def _handle_func_googleapiclient(error, message, error_dict, http_response):
"""Constructs a ``FirebaseError`` from the given GCP 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``.
error_dict: Parsed GCP error response.
http_response: A requests HTTP response object to associate with the exception.

Returns:
FirebaseError: A ``FirebaseError`` that can be raised to the user code or None.
"""
code = error_dict.get('status')
return handle_googleapiclient_error(error, message, code, http_response)


def handle_googleapiclient_error(error, message=None, code=None, http_response=None):
"""Constructs a ``FirebaseError`` from the given googleapiclient error.

This method is agnostic of the remote service that produced the error, whether it is a GCP
service or otherwise. Therefore, this method does not attempt to parse the error response in
any way.

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: A 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 to determine a
suitable error code.
http_response: A requests HTTP response object to associate with the exception (optional).
If not specified, one will be created from the ``error``.

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 = _http_status_to_error_code(error.resp.status)
if not message:
message = str(error)
if not http_response:
http_response = _http_response_from_googleapiclient_error(error)

err_type = _error_code_to_exception_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 _http_status_to_error_code(status):
"""Maps an HTTP status to a platform error code."""
return _HTTP_STATUS_TO_ERROR_CODE.get(status, exceptions.UNKNOWN)


def _error_code_to_exception_type(code):
"""Maps a platform error code to an exception type."""
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', {})
msg = error_dict.get('message')
if not msg:
msg = 'Unexpected HTTP response with status: {0}; body: {1}'.format(status_code, content)
return error_dict, msg
Loading