Skip to content

Commit f69e14c

Browse files
authored
Introduced the exceptions module (#296)
* Added the exceptions module * Cleaned up the error handling logic; Added tests * Updated docs; Fixed some typos
1 parent f2dd24e commit f69e14c

File tree

7 files changed

+376
-24
lines changed

7 files changed

+376
-24
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Unreleased
22

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

58
# v2.17.0
69

firebase_admin/_utils.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,22 @@
1414

1515
"""Internal utilities common to all modules."""
1616

17+
import requests
18+
1719
import firebase_admin
20+
from firebase_admin import exceptions
21+
22+
23+
_STATUS_TO_EXCEPTION_TYPE = {
24+
400: exceptions.InvalidArgumentError,
25+
401: exceptions.UnauthenticatedError,
26+
403: exceptions.PermissionDeniedError,
27+
404: exceptions.NotFoundError,
28+
409: exceptions.ConflictError,
29+
429: exceptions.ResourceExhaustedError,
30+
500: exceptions.InternalError,
31+
503: exceptions.UnavailableError,
32+
}
1833

1934

2035
def _get_initialized_app(app):
@@ -33,3 +48,36 @@ def _get_initialized_app(app):
3348
def get_app_service(app, name, initializer):
3449
app = _get_initialized_app(app)
3550
return app._get_service(name, initializer) # pylint: disable=protected-access
51+
52+
def handle_requests_error(error, message=None, status=None):
53+
"""Constructs a ``FirebaseError`` from the given requests error.
54+
55+
Args:
56+
error: An error raised by the reqests module while making an HTTP call.
57+
message: A message to be included in the resulting ``FirebaseError`` (optional). If not
58+
specified the string representation of the ``error`` argument is used as the message.
59+
status: An HTTP status code that will be used to determine the resulting error type
60+
(optional). If not specified the HTTP status code on the error response is used.
61+
62+
Returns:
63+
FirebaseError: A ``FirebaseError`` that can be raised to the user code.
64+
"""
65+
if isinstance(error, requests.exceptions.Timeout):
66+
return exceptions.DeadlineExceededError(
67+
message='Timed out while making an API call: {0}'.format(error),
68+
cause=error)
69+
elif isinstance(error, requests.exceptions.ConnectionError):
70+
return exceptions.UnavailableError(
71+
message='Failed to establish a connection: {0}'.format(error),
72+
cause=error)
73+
elif error.response is None:
74+
return exceptions.UnknownError(
75+
message='Unknown error while making a remote service call: {0}'.format(error),
76+
cause=error)
77+
78+
if not status:
79+
status = error.response.status_code
80+
if not message:
81+
message = str(error)
82+
err_type = _STATUS_TO_EXCEPTION_TYPE.get(status, exceptions.UnknownError)
83+
return err_type(message=message, cause=error, http_response=error.response)

firebase_admin/exceptions.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Copyright 2019 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Firebase Exceptions module.
16+
17+
This module defines the base types for exceptions and the platform-wide error codes as outlined in
18+
https://cloud.google.com/apis/design/errors.
19+
"""
20+
21+
22+
INVALID_ARGUMENT = 'INVALID_ARGUMENT'
23+
FAILED_PRECONDITION = 'FAILED_PRECONDITION'
24+
OUT_OF_RANGE = 'OUT_OF_RANGE'
25+
UNAUTHENTICATED = 'UNAUTHENTICATED'
26+
PERMISSION_DENIED = 'PERMISSION_DENIED'
27+
NOT_FOUND = 'NOT_FOUND'
28+
CONFLICT = 'CONFLICT'
29+
ABORTED = 'ABORTED'
30+
ALREADY_EXISTS = 'ALREADY_EXISTS'
31+
RESOURCE_EXHAUSTED = 'RESOURCE_EXHAUSTED'
32+
CANCELLED = 'CANCELLED'
33+
DATA_LOSS = 'DATA_LOSS'
34+
UNKNOWN = 'UNKNOWN'
35+
INTERNAL = 'INTERNAL'
36+
UNAVAILABLE = 'UNAVAILABLE'
37+
DEADLINE_EXCEEDED = 'DEADLINE_EXCEEDED'
38+
39+
40+
class FirebaseError(Exception):
41+
"""Base class for all errors raised by the Admin SDK."""
42+
43+
def __init__(self, code, message, cause=None, http_response=None):
44+
Exception.__init__(self, message)
45+
self._code = code
46+
self._cause = cause
47+
self._http_response = http_response
48+
49+
@property
50+
def code(self):
51+
return self._code
52+
53+
@property
54+
def cause(self):
55+
return self._cause
56+
57+
@property
58+
def http_response(self):
59+
return self._http_response
60+
61+
62+
class InvalidArgumentError(FirebaseError):
63+
"""Client specified an invalid argument."""
64+
65+
def __init__(self, message, cause=None, http_response=None):
66+
FirebaseError.__init__(self, INVALID_ARGUMENT, message, cause, http_response)
67+
68+
69+
class FailedPreconditionError(FirebaseError):
70+
"""Request can not be executed in the current system state, such as deleting a non-empty
71+
directory."""
72+
73+
def __init__(self, message, cause=None, http_response=None):
74+
FirebaseError.__init__(self, FAILED_PRECONDITION, message, cause, http_response)
75+
76+
77+
class OutOfRangeError(FirebaseError):
78+
"""Client specified an invalid range."""
79+
80+
def __init__(self, message, cause=None, http_response=None):
81+
FirebaseError.__init__(self, OUT_OF_RANGE, message, cause, http_response)
82+
83+
84+
class UnauthenticatedError(FirebaseError):
85+
"""Request not authenticated due to missing, invalid, or expired OAuth token."""
86+
87+
def __init__(self, message, cause=None, http_response=None):
88+
FirebaseError.__init__(self, UNAUTHENTICATED, message, cause, http_response)
89+
90+
91+
class PermissionDeniedError(FirebaseError):
92+
"""Client does not have sufficient permission.
93+
94+
This can happen because the OAuth token does not have the right scopes, the client doesn't
95+
have permission, or the API has not been enabled for the client project.
96+
"""
97+
98+
def __init__(self, message, cause=None, http_response=None):
99+
FirebaseError.__init__(self, PERMISSION_DENIED, message, cause, http_response)
100+
101+
102+
class NotFoundError(FirebaseError):
103+
"""A specified resource is not found, or the request is rejected by undisclosed reasons, such
104+
as whitelisting."""
105+
106+
def __init__(self, message, cause=None, http_response=None):
107+
FirebaseError.__init__(self, NOT_FOUND, message, cause, http_response)
108+
109+
110+
class ConflictError(FirebaseError):
111+
"""Concurrency conflict, such as read-modify-write conflict."""
112+
113+
def __init__(self, message, cause=None, http_response=None):
114+
FirebaseError.__init__(self, CONFLICT, message, cause, http_response)
115+
116+
117+
class AbortedError(FirebaseError):
118+
"""Concurrency conflict, such as read-modify-write conflict."""
119+
120+
def __init__(self, message, cause=None, http_response=None):
121+
FirebaseError.__init__(self, ABORTED, message, cause, http_response)
122+
123+
124+
class AlreadyExistsError(FirebaseError):
125+
"""The resource that a client tried to create already exists."""
126+
127+
def __init__(self, message, cause=None, http_response=None):
128+
FirebaseError.__init__(self, ALREADY_EXISTS, message, cause, http_response)
129+
130+
131+
class ResourceExhaustedError(FirebaseError):
132+
"""Either out of resource quota or reaching rate limiting."""
133+
134+
def __init__(self, message, cause=None, http_response=None):
135+
FirebaseError.__init__(self, RESOURCE_EXHAUSTED, message, cause, http_response)
136+
137+
138+
class CancelledError(FirebaseError):
139+
"""Request cancelled by the client."""
140+
141+
def __init__(self, message, cause=None, http_response=None):
142+
FirebaseError.__init__(self, CANCELLED, message, cause, http_response)
143+
144+
145+
class DataLossError(FirebaseError):
146+
"""Unrecoverable data loss or data corruption."""
147+
148+
def __init__(self, message, cause=None, http_response=None):
149+
FirebaseError.__init__(self, DATA_LOSS, message, cause, http_response)
150+
151+
152+
class UnknownError(FirebaseError):
153+
"""Unknown server error."""
154+
155+
def __init__(self, message, cause=None, http_response=None):
156+
FirebaseError.__init__(self, UNKNOWN, message, cause, http_response)
157+
158+
159+
class InternalError(FirebaseError):
160+
"""Internal server error."""
161+
162+
def __init__(self, message, cause=None, http_response=None):
163+
FirebaseError.__init__(self, INTERNAL, message, cause, http_response)
164+
165+
166+
class UnavailableError(FirebaseError):
167+
"""Service unavailable. Typically the server is down."""
168+
169+
def __init__(self, message, cause=None, http_response=None):
170+
FirebaseError.__init__(self, UNAVAILABLE, message, cause, http_response)
171+
172+
173+
class DeadlineExceededError(FirebaseError):
174+
"""Request deadline exceeded.
175+
176+
This will happen only if the caller sets a deadline that is shorter than the method's
177+
default deadline (i.e. requested deadline is not enough for the server to process the
178+
request) and the request did not finish within the deadline.
179+
"""
180+
181+
def __init__(self, message, cause=None, http_response=None):
182+
FirebaseError.__init__(self, DEADLINE_EXCEEDED, message, cause, http_response)

firebase_admin/instance_id.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,6 @@ def delete_instance_id(instance_id, app=None):
5353
_get_iid_service(app).delete_instance_id(instance_id)
5454

5555

56-
class ApiCallError(Exception):
57-
"""Represents an Exception encountered while invoking the Firebase instance ID service."""
58-
59-
def __init__(self, message, error):
60-
Exception.__init__(self, message)
61-
self.detail = error
62-
63-
6456
class _InstanceIdService(object):
6557
"""Provides methods for interacting with the remote instance ID service."""
6658

@@ -94,14 +86,15 @@ def delete_instance_id(self, instance_id):
9486
try:
9587
self._client.request('delete', path)
9688
except requests.exceptions.RequestException as error:
97-
raise ApiCallError(self._extract_message(instance_id, error), error)
89+
msg = self._extract_message(instance_id, error)
90+
raise _utils.handle_requests_error(error, msg)
9891

9992
def _extract_message(self, instance_id, error):
10093
if error.response is None:
101-
return str(error)
94+
return None
10295
status = error.response.status_code
10396
msg = self.error_codes.get(status)
10497
if msg:
10598
return 'Instance ID "{0}": {1}'.format(instance_id, msg)
10699
else:
107-
return str(error)
100+
return 'Instance ID "{0}": {1}'.format(instance_id, error)

integration/test_instance_id.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616

1717
import pytest
1818

19+
from firebase_admin import exceptions
1920
from firebase_admin import instance_id
2021

2122
def test_delete_non_existing():
22-
with pytest.raises(instance_id.ApiCallError) as excinfo:
23+
with pytest.raises(exceptions.NotFoundError) as excinfo:
2324
# legal instance IDs are /[cdef][A-Za-z0-9_-]{9}[AEIMQUYcgkosw048]/
2425
instance_id.delete_instance_id('fictive-ID0')
2526
assert str(excinfo.value) == 'Instance ID "fictive-ID0": Failed to find the instance ID.'

tests/test_exceptions.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Copyright 2019 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
import requests
17+
from requests import models
18+
19+
from firebase_admin import exceptions
20+
from firebase_admin import _utils
21+
22+
23+
def test_timeout_error():
24+
error = requests.exceptions.Timeout('Test error')
25+
firebase_error = _utils.handle_requests_error(error)
26+
assert isinstance(firebase_error, exceptions.DeadlineExceededError)
27+
assert str(firebase_error) == 'Timed out while making an API call: Test error'
28+
assert firebase_error.cause is error
29+
assert firebase_error.http_response is None
30+
31+
def test_connection_error():
32+
error = requests.exceptions.ConnectionError('Test error')
33+
firebase_error = _utils.handle_requests_error(error)
34+
assert isinstance(firebase_error, exceptions.UnavailableError)
35+
assert str(firebase_error) == 'Failed to establish a connection: Test error'
36+
assert firebase_error.cause is error
37+
assert firebase_error.http_response is None
38+
39+
def test_unknown_transport_error():
40+
error = requests.exceptions.RequestException('Test error')
41+
firebase_error = _utils.handle_requests_error(error)
42+
assert isinstance(firebase_error, exceptions.UnknownError)
43+
assert str(firebase_error) == 'Unknown error while making a remote service call: Test error'
44+
assert firebase_error.cause is error
45+
assert firebase_error.http_response is None
46+
47+
def test_http_response():
48+
resp = models.Response()
49+
resp.status_code = 500
50+
error = requests.exceptions.RequestException('Test error', response=resp)
51+
firebase_error = _utils.handle_requests_error(error)
52+
assert isinstance(firebase_error, exceptions.InternalError)
53+
assert str(firebase_error) == 'Test error'
54+
assert firebase_error.cause is error
55+
assert firebase_error.http_response is resp
56+
57+
def test_http_response_with_unknown_status():
58+
resp = models.Response()
59+
resp.status_code = 501
60+
error = requests.exceptions.RequestException('Test error', response=resp)
61+
firebase_error = _utils.handle_requests_error(error)
62+
assert isinstance(firebase_error, exceptions.UnknownError)
63+
assert str(firebase_error) == 'Test error'
64+
assert firebase_error.cause is error
65+
assert firebase_error.http_response is resp
66+
67+
def test_http_response_with_message():
68+
resp = models.Response()
69+
resp.status_code = 500
70+
error = requests.exceptions.RequestException('Test error', response=resp)
71+
firebase_error = _utils.handle_requests_error(error, message='Explicit error message')
72+
assert isinstance(firebase_error, exceptions.InternalError)
73+
assert str(firebase_error) == 'Explicit error message'
74+
assert firebase_error.cause is error
75+
assert firebase_error.http_response is resp
76+
77+
def test_http_response_with_status():
78+
resp = models.Response()
79+
resp.status_code = 500
80+
error = requests.exceptions.RequestException('Test error', response=resp)
81+
firebase_error = _utils.handle_requests_error(error, status=503)
82+
assert isinstance(firebase_error, exceptions.UnavailableError)
83+
assert str(firebase_error) == 'Test error'
84+
assert firebase_error.cause is error
85+
assert firebase_error.http_response is resp
86+
87+
def test_http_response_with_message_and_status():
88+
resp = models.Response()
89+
resp.status_code = 500
90+
error = requests.exceptions.RequestException('Test error', response=resp)
91+
firebase_error = _utils.handle_requests_error(
92+
error, message='Explicit error message', status=503)
93+
assert isinstance(firebase_error, exceptions.UnavailableError)
94+
assert str(firebase_error) == 'Explicit error message'
95+
assert firebase_error.cause is error
96+
assert firebase_error.http_response is resp

0 commit comments

Comments
 (0)