Skip to content

Commit fa843f3

Browse files
authored
Migrated remaining messaging APIs to new error types (#298)
* Migrated FCM send APIs to the new error handling regime * Moved error parsing logic to _utils * Refactored OP error handling code * Fixing a broken test * Added utils for handling googleapiclient errors * Added tests for new error handling logic * Updated public API docs * Fixing test for python3 * Cleaning up the error code lookup code * Cleaning up the error parsing APIs * Cleaned up error parsing logic; Updated docs * Migrated the FCM IID APIs to the new error types
1 parent 2879a22 commit fa843f3

File tree

2 files changed

+27
-60
lines changed

2 files changed

+27
-60
lines changed

firebase_admin/messaging.py

Lines changed: 14 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
'AndroidNotification',
3737
'APNSConfig',
3838
'APNSPayload',
39-
'ApiCallError',
4039
'Aps',
4140
'ApsAlert',
4241
'BatchResponse',
@@ -45,8 +44,12 @@
4544
'Message',
4645
'MulticastMessage',
4746
'Notification',
47+
'QuotaExceededError',
48+
'SenderIdMismatchError',
4849
'SendResponse',
50+
'ThirdPartyAuthError',
4951
'TopicManagementResponse',
52+
'UnregisteredError',
5053
'WebpushConfig',
5154
'WebpushFcmOptions',
5255
'WebpushNotification',
@@ -167,7 +170,7 @@ def subscribe_to_topic(tokens, topic, app=None):
167170
TopicManagementResponse: A ``TopicManagementResponse`` instance.
168171
169172
Raises:
170-
ApiCallError: If an error occurs while communicating with instance ID service.
173+
FirebaseError: If an error occurs while communicating with instance ID service.
171174
ValueError: If the input arguments are invalid.
172175
"""
173176
return _get_messaging_service(app).make_topic_management_request(
@@ -186,7 +189,7 @@ def unsubscribe_from_topic(tokens, topic, app=None):
186189
TopicManagementResponse: A ``TopicManagementResponse`` instance.
187190
188191
Raises:
189-
ApiCallError: If an error occurs while communicating with instance ID service.
192+
FirebaseError: If an error occurs while communicating with instance ID service.
190193
ValueError: If the input arguments are invalid.
191194
"""
192195
return _get_messaging_service(app).make_topic_management_request(
@@ -243,21 +246,6 @@ def errors(self):
243246
return self._errors
244247

245248

246-
class ApiCallError(Exception):
247-
"""Represents an Exception encountered while invoking the FCM API.
248-
249-
Attributes:
250-
code: A string error code.
251-
message: A error message string.
252-
detail: Original low-level exception.
253-
"""
254-
255-
def __init__(self, code, message, detail=None):
256-
Exception.__init__(self, message)
257-
self.code = code
258-
self.detail = detail
259-
260-
261249
class BatchResponse(object):
262250
"""The response received from a batch request to the FCM API."""
263251

@@ -300,7 +288,7 @@ def success(self):
300288

301289
@property
302290
def exception(self):
303-
"""An ApiCallError if an error occurs while sending the message to the FCM service."""
291+
"""A FirebaseError if an error occurs while sending the message to the FCM service."""
304292
return self._exception
305293

306294

@@ -313,22 +301,13 @@ class _MessagingService(object):
313301
IID_HEADERS = {'access_token_auth': 'true'}
314302
JSON_ENCODER = _messaging_utils.MessageEncoder()
315303

316-
INTERNAL_ERROR = 'internal-error'
317-
UNKNOWN_ERROR = 'unknown-error'
318304
FCM_ERROR_TYPES = {
319305
'APNS_AUTH_ERROR': ThirdPartyAuthError,
320306
'QUOTA_EXCEEDED': QuotaExceededError,
321307
'SENDER_ID_MISMATCH': SenderIdMismatchError,
322308
'THIRD_PARTY_AUTH_ERROR': ThirdPartyAuthError,
323309
'UNREGISTERED': UnregisteredError,
324310
}
325-
IID_ERROR_CODES = {
326-
400: 'invalid-argument',
327-
401: 'authentication-error',
328-
403: 'authentication-error',
329-
500: INTERNAL_ERROR,
330-
503: 'server-unavailable',
331-
}
332311

333312
def __init__(self, app):
334313
project_id = app.project_id
@@ -431,10 +410,7 @@ def make_topic_management_request(self, tokens, topic, operation):
431410
timeout=self._timeout
432411
)
433412
except requests.exceptions.RequestException as error:
434-
if error.response is not None:
435-
self._handle_iid_error(error)
436-
else:
437-
raise ApiCallError(self.INTERNAL_ERROR, 'Failed to call instance ID API.', error)
413+
raise self._handle_iid_error(error)
438414
else:
439415
return TopicManagementResponse(resp)
440416

@@ -456,6 +432,9 @@ def _handle_fcm_error(self, error):
456432

457433
def _handle_iid_error(self, error):
458434
"""Handles errors received from the Instance ID API."""
435+
if error.response is None:
436+
raise _utils.handle_requests_error(error)
437+
459438
data = {}
460439
try:
461440
parsed_body = error.response.json()
@@ -464,13 +443,13 @@ def _handle_iid_error(self, error):
464443
except ValueError:
465444
pass
466445

467-
code = _MessagingService.IID_ERROR_CODES.get(
468-
error.response.status_code, _MessagingService.UNKNOWN_ERROR)
446+
# IID error response format: {"error": "some error message"}
469447
msg = data.get('error')
470448
if not msg:
471449
msg = 'Unexpected HTTP response with status: {0}; body: {1}'.format(
472450
error.response.status_code, error.response.content.decode())
473-
raise ApiCallError(code, msg, error)
451+
452+
return _utils.handle_requests_error(error, msg)
474453

475454
def _handle_batch_error(self, error):
476455
"""Handles errors received from the googleapiclient while making batch requests."""

tests/test_messaging.py

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@
3232
NON_DICT_ARGS = ['', list(), tuple(), True, False, 1, 0, {1: 'foo'}, {'foo': 1}]
3333
NON_OBJECT_ARGS = [list(), tuple(), dict(), 'foo', 0, 1, True, False]
3434
NON_LIST_ARGS = ['', tuple(), dict(), True, False, 1, 0, [1], ['foo', 1]]
35-
HTTP_ERRORS = [400, 404, 500] # TODO(hkj): Remove this when IID tests are updated.
3635
HTTP_ERROR_CODES = {
3736
400: exceptions.InvalidArgumentError,
37+
403: exceptions.PermissionDeniedError,
3838
404: exceptions.NotFoundError,
3939
500: exceptions.InternalError,
4040
503: exceptions.UnavailableError,
@@ -1859,30 +1859,24 @@ def test_subscribe_to_topic(self, args):
18591859
assert recorder[0].url == self._get_url('iid/v1:batchAdd')
18601860
assert json.loads(recorder[0].body.decode()) == args[2]
18611861

1862-
@pytest.mark.parametrize('status', HTTP_ERRORS)
1863-
def test_subscribe_to_topic_error(self, status):
1862+
@pytest.mark.parametrize('status, exc_type', HTTP_ERROR_CODES.items())
1863+
def test_subscribe_to_topic_error(self, status, exc_type):
18641864
_, recorder = self._instrument_iid_service(
18651865
status=status, payload=self._DEFAULT_ERROR_RESPONSE)
1866-
with pytest.raises(messaging.ApiCallError) as excinfo:
1866+
with pytest.raises(exc_type) as excinfo:
18671867
messaging.subscribe_to_topic('foo', 'test-topic')
18681868
assert str(excinfo.value) == 'error_reason'
1869-
code = messaging._MessagingService.IID_ERROR_CODES.get(
1870-
status, messaging._MessagingService.UNKNOWN_ERROR)
1871-
assert excinfo.value.code == code
18721869
assert len(recorder) == 1
18731870
assert recorder[0].method == 'POST'
18741871
assert recorder[0].url == self._get_url('iid/v1:batchAdd')
18751872

1876-
@pytest.mark.parametrize('status', HTTP_ERRORS)
1877-
def test_subscribe_to_topic_non_json_error(self, status):
1873+
@pytest.mark.parametrize('status, exc_type', HTTP_ERROR_CODES.items())
1874+
def test_subscribe_to_topic_non_json_error(self, status, exc_type):
18781875
_, recorder = self._instrument_iid_service(status=status, payload='not json')
1879-
with pytest.raises(messaging.ApiCallError) as excinfo:
1876+
with pytest.raises(exc_type) as excinfo:
18801877
messaging.subscribe_to_topic('foo', 'test-topic')
18811878
reason = 'Unexpected HTTP response with status: {0}; body: not json'.format(status)
1882-
code = messaging._MessagingService.IID_ERROR_CODES.get(
1883-
status, messaging._MessagingService.UNKNOWN_ERROR)
18841879
assert str(excinfo.value) == reason
1885-
assert excinfo.value.code == code
18861880
assert len(recorder) == 1
18871881
assert recorder[0].method == 'POST'
18881882
assert recorder[0].url == self._get_url('iid/v1:batchAdd')
@@ -1897,30 +1891,24 @@ def test_unsubscribe_from_topic(self, args):
18971891
assert recorder[0].url == self._get_url('iid/v1:batchRemove')
18981892
assert json.loads(recorder[0].body.decode()) == args[2]
18991893

1900-
@pytest.mark.parametrize('status', HTTP_ERRORS)
1901-
def test_unsubscribe_from_topic_error(self, status):
1894+
@pytest.mark.parametrize('status, exc_type', HTTP_ERROR_CODES.items())
1895+
def test_unsubscribe_from_topic_error(self, status, exc_type):
19021896
_, recorder = self._instrument_iid_service(
19031897
status=status, payload=self._DEFAULT_ERROR_RESPONSE)
1904-
with pytest.raises(messaging.ApiCallError) as excinfo:
1898+
with pytest.raises(exc_type) as excinfo:
19051899
messaging.unsubscribe_from_topic('foo', 'test-topic')
19061900
assert str(excinfo.value) == 'error_reason'
1907-
code = messaging._MessagingService.IID_ERROR_CODES.get(
1908-
status, messaging._MessagingService.UNKNOWN_ERROR)
1909-
assert excinfo.value.code == code
19101901
assert len(recorder) == 1
19111902
assert recorder[0].method == 'POST'
19121903
assert recorder[0].url == self._get_url('iid/v1:batchRemove')
19131904

1914-
@pytest.mark.parametrize('status', HTTP_ERRORS)
1915-
def test_unsubscribe_from_topic_non_json_error(self, status):
1905+
@pytest.mark.parametrize('status, exc_type', HTTP_ERROR_CODES.items())
1906+
def test_unsubscribe_from_topic_non_json_error(self, status, exc_type):
19161907
_, recorder = self._instrument_iid_service(status=status, payload='not json')
1917-
with pytest.raises(messaging.ApiCallError) as excinfo:
1908+
with pytest.raises(exc_type) as excinfo:
19181909
messaging.unsubscribe_from_topic('foo', 'test-topic')
19191910
reason = 'Unexpected HTTP response with status: {0}; body: not json'.format(status)
1920-
code = messaging._MessagingService.IID_ERROR_CODES.get(
1921-
status, messaging._MessagingService.UNKNOWN_ERROR)
19221911
assert str(excinfo.value) == reason
1923-
assert excinfo.value.code == code
19241912
assert len(recorder) == 1
19251913
assert recorder[0].method == 'POST'
19261914
assert recorder[0].url == self._get_url('iid/v1:batchRemove')

0 commit comments

Comments
 (0)