Skip to content

Error handling revamp (v3 release) #334

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 22 commits into from
Sep 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f69e14c
Introduced the exceptions module (#296)
hiranya911 Jun 12, 2019
2879a22
Migrating FCM Send APIs to the New Exceptions (#297)
hiranya911 Jun 20, 2019
fa843f3
Migrated remaining messaging APIs to new error types (#298)
hiranya911 Jun 26, 2019
b27216b
Introducing TokenSignError to represent custom token creation errors …
hiranya911 Jul 5, 2019
99929ed
Raising FirebaseError from create_session_cookie() API (#306)
hiranya911 Jul 18, 2019
9fb0766
Introducing UserNotFoundError type (#309)
hiranya911 Jul 18, 2019
8a0cf08
New error handling support in create/update/delete user APIs (#311)
hiranya911 Jul 26, 2019
29c8b7a
Error handling improvements in email action link APIs (#312)
hiranya911 Jul 31, 2019
3361452
Project management API migrated to new error types (#314)
hiranya911 Jul 31, 2019
dbb6970
Error handling updated for remaining user_mgt APIs (#315)
hiranya911 Aug 2, 2019
baf4991
Merged with master
hiranya911 Aug 3, 2019
1210723
Migrated token verification APIs to new exception types (#317)
hiranya911 Aug 5, 2019
299e808
Migrated the db module to the new exception types (#318)
hiranya911 Aug 5, 2019
030f6e6
Adding a few overlooked error types (#319)
hiranya911 Aug 8, 2019
7974c05
Removing the ability to delete user properties by passing None (#320)
hiranya911 Aug 9, 2019
cfff7d4
Merge branch 'master' into error-handling-revamp
hiranya911 Aug 20, 2019
ff28261
Some types renamed to be PEP8 compliant (#330)
hiranya911 Aug 20, 2019
8bf4118
Upgraded Cloud Firestore and Cloud Storage dependencies (#325)
hiranya911 Aug 26, 2019
a0bc210
Merge branch 'master' into error-handling-revamp
hiranya911 Aug 27, 2019
8373c21
Added documentation for error codes (#339)
hiranya911 Sep 5, 2019
3393269
A few API doc updates (#340)
hiranya911 Sep 10, 2019
88af437
Merge branch 'master' into error-handling-revamp
hiranya911 Sep 10, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Unreleased

-
- [added] Added the new `firebase_admin.exceptions` module containing the
base exception types and global error codes.
- [changed] Updated the `firebase_admin.instance_id` module to use the new
shared exception types. The type `instance_id.ApiCallError` was removed.

# v2.18.0

Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ pip install firebase-admin

Please refer to the [CONTRIBUTING page](./CONTRIBUTING.md) for more information
about how you can contribute to this project. We welcome bug reports, feature
requests, code review feedback, and also pull requests.
requests, code review feedback, and also pull requests.


## Supported Python Versions

We support Python 2.7 and Python 3.3+. Firebase Admin Python SDK is also tested
on PyPy and [Google App Engine](https://cloud.google.com/appengine/) environments.
We currently support Python 2.7 and Python 3.4+. However, Python 2.7 support is
being phased out, and the developers are advised to use latest Python 3.
Firebase Admin Python SDK is also tested on PyPy and
[Google App Engine](https://cloud.google.com/appengine/) environments.


## Documentation
Expand Down
121 changes: 121 additions & 0 deletions firebase_admin/_auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import six
from six.moves import urllib

from firebase_admin import exceptions
from firebase_admin import _utils


MAX_CLAIMS_PAYLOAD_SIZE = 1000
RESERVED_CLAIMS = set([
Expand Down Expand Up @@ -188,3 +191,121 @@ def validate_action_type(action_type):
raise ValueError('Invalid action type provided action_type: {0}. \
Valid values are {1}'.format(action_type, ', '.join(VALID_EMAIL_ACTION_TYPES)))
return action_type


class UidAlreadyExistsError(exceptions.AlreadyExistsError):
"""The user with the provided uid already exists."""

default_message = 'The user with the provided uid already exists'

def __init__(self, message, cause, http_response):
exceptions.AlreadyExistsError.__init__(self, message, cause, http_response)


class EmailAlreadyExistsError(exceptions.AlreadyExistsError):
"""The user with the provided email already exists."""

default_message = 'The user with the provided email already exists'

def __init__(self, message, cause, http_response):
exceptions.AlreadyExistsError.__init__(self, message, cause, http_response)


class InvalidDynamicLinkDomainError(exceptions.InvalidArgumentError):
"""Dynamic link domain in ActionCodeSettings is not authorized."""

default_message = 'Dynamic link domain specified in ActionCodeSettings is not authorized'

def __init__(self, message, cause, http_response):
exceptions.InvalidArgumentError.__init__(self, message, cause, http_response)


class InvalidIdTokenError(exceptions.InvalidArgumentError):
"""The provided ID token is not a valid Firebase ID token."""

default_message = 'The provided ID token is invalid'

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


class PhoneNumberAlreadyExistsError(exceptions.AlreadyExistsError):
"""The user with the provided phone number already exists."""

default_message = 'The user with the provided phone number already exists'

def __init__(self, message, cause, http_response):
exceptions.AlreadyExistsError.__init__(self, message, cause, http_response)


class UnexpectedResponseError(exceptions.UnknownError):
"""Backend service responded with an unexpected or malformed response."""

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


class UserNotFoundError(exceptions.NotFoundError):
"""No user record found for the specified identifier."""

default_message = 'No user record found for the given identifier'

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


_CODE_TO_EXC_TYPE = {
'DUPLICATE_EMAIL': EmailAlreadyExistsError,
'DUPLICATE_LOCAL_ID': UidAlreadyExistsError,
'INVALID_DYNAMIC_LINK_DOMAIN': InvalidDynamicLinkDomainError,
'INVALID_ID_TOKEN': InvalidIdTokenError,
'PHONE_NUMBER_EXISTS': PhoneNumberAlreadyExistsError,
'USER_NOT_FOUND': UserNotFoundError,
}


def handle_auth_backend_error(error):
"""Converts a requests error received from the Firebase Auth service into a FirebaseError."""
if error.response is None:
raise _utils.handle_requests_error(error)

code, custom_message = _parse_error_body(error.response)
if not code:
msg = 'Unexpected error response: {0}'.format(error.response.content.decode())
raise _utils.handle_requests_error(error, message=msg)

exc_type = _CODE_TO_EXC_TYPE.get(code)
msg = _build_error_message(code, exc_type, custom_message)
if not exc_type:
return _utils.handle_requests_error(error, message=msg)

return exc_type(msg, cause=error, http_response=error.response)


def _parse_error_body(response):
"""Parses the given error response to extract Auth error code and message."""
error_dict = {}
try:
parsed_body = response.json()
if isinstance(parsed_body, dict):
error_dict = parsed_body.get('error', {})
except ValueError:
pass

# Auth error response format: {"error": {"message": "AUTH_ERROR_CODE: Optional text"}}
code = error_dict.get('message') if isinstance(error_dict, dict) else None
custom_message = None
if code:
separator = code.find(':')
if separator != -1:
custom_message = code[separator + 1:].strip()
code = code[:separator]

return code, custom_message


def _build_error_message(code, exc_type, custom_message):
default_message = exc_type.default_message if (
exc_type and hasattr(exc_type, 'default_message')) else 'Error while calling Auth service'
ext = ' {0}'.format(custom_message) if custom_message else ''
return '{0} ({1}).{2}'.format(default_message, code, ext)
4 changes: 4 additions & 0 deletions firebase_admin/_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ def headers(self, method, url, **kwargs):
resp = self.request(method, url, **kwargs)
return resp.headers

def body_and_response(self, method, url, **kwargs):
resp = self.request(method, url, **kwargs)
return self.parse_body(resp), resp

def body(self, method, url, **kwargs):
resp = self.request(method, url, **kwargs)
return self.parse_body(resp)
Expand Down
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 @@ -921,3 +923,33 @@ def encode_fcm_options(cls, fcm_options):
}
result = cls.remove_null_values(result)
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)
Loading