Skip to content

Commit 1ea9ab6

Browse files
committed
Upgrade requests exceptions to AnymailRequestsAPIError
Catch and re-raise requests.RequestException in AnymailRequestsBackend.post_to_esp. * AnymailRequestsAPIError is needed for proper fail_silently handling. * Retain original requests exception type, to avoid breaking existing code that might look for specific requests exceptions. Closes #16
1 parent 5d2bc66 commit 1ea9ab6

File tree

3 files changed

+34
-2
lines changed

3 files changed

+34
-2
lines changed

anymail/backends/base_requests.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,15 @@ def post_to_esp(self, payload, message):
6565
Can raise AnymailRequestsAPIError for HTTP errors in the post
6666
"""
6767
params = payload.get_request_params(self.api_url)
68-
response = self.session.request(**params)
68+
try:
69+
response = self.session.request(**params)
70+
except requests.RequestException as err:
71+
# raise an exception that is both AnymailRequestsAPIError
72+
# and the original requests exception type
73+
exc_class = type('AnymailRequestsAPIError', (AnymailRequestsAPIError, type(err)), {})
74+
raise exc_class(
75+
"Error posting to %s:" % params.get('url', '<missing url>'),
76+
raised_from=err, email_message=message, payload=payload)
6977
self.raise_for_status(response, payload, message)
7078
return response
7179

anymail/exceptions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from traceback import format_exception_only
23

34
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
45
from requests import HTTPError
@@ -18,11 +19,13 @@ def __init__(self, *args, **kwargs):
1819
status_code: HTTP status code of response to ESP send call
1920
payload: data arg (*not* json-stringified) for the ESP send call
2021
response: requests.Response from the send call
22+
raised_from: original/wrapped Exception
2123
"""
2224
self.backend = kwargs.pop('backend', None)
2325
self.email_message = kwargs.pop('email_message', None)
2426
self.payload = kwargs.pop('payload', None)
2527
self.status_code = kwargs.pop('status_code', None)
28+
self.raised_from = kwargs.pop('raised_from', None)
2629
if isinstance(self, HTTPError):
2730
# must leave response in kwargs for HTTPError
2831
self.response = kwargs.get('response', None)
@@ -33,6 +36,7 @@ def __init__(self, *args, **kwargs):
3336
def __str__(self):
3437
parts = [
3538
" ".join([str(arg) for arg in self.args]),
39+
self.describe_raised_from(),
3640
self.describe_send(),
3741
self.describe_response(),
3842
]
@@ -68,6 +72,12 @@ def describe_response(self):
6872
pass
6973
return description
7074

75+
def describe_raised_from(self):
76+
"""Return the original exception"""
77+
if self.raised_from is None:
78+
return None
79+
return ''.join(format_exception_only(type(self.raised_from), self.raised_from)).strip()
80+
7181

7282
class AnymailAPIError(AnymailError):
7383
"""Exception for unsuccessful response from ESP's API."""

tests/test_mailgun_backend.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from django.test.utils import override_settings
1313
from django.utils.timezone import get_fixed_timezone, override as override_current_timezone
1414

15-
from anymail.exceptions import AnymailAPIError, AnymailUnsupportedFeature
15+
from anymail.exceptions import AnymailAPIError, AnymailRequestsAPIError, AnymailUnsupportedFeature
1616
from anymail.message import attach_inline_image_file
1717

1818
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin
@@ -258,6 +258,20 @@ def test_api_error_includes_details(self):
258258
with self.assertRaises(AnymailAPIError):
259259
self.message.send()
260260

261+
def test_requests_exception(self):
262+
"""Exception during API call should be AnymailAPIError"""
263+
# (The post itself raises an error -- different from returning a failure response)
264+
from requests.exceptions import SSLError # a low-level requests exception
265+
self.mock_request.side_effect = SSLError("Something bad")
266+
with self.assertRaisesMessage(AnymailRequestsAPIError, "Something bad") as cm:
267+
self.message.send()
268+
self.assertIsInstance(cm.exception, SSLError) # also retains specific requests exception class
269+
270+
# Make sure fail_silently is respected
271+
self.mock_request.side_effect = SSLError("Something bad")
272+
sent = mail.send_mail('Subject', 'Body', '[email protected]', ['[email protected]'], fail_silently=True)
273+
self.assertEqual(sent, 0)
274+
261275

262276
class MailgunBackendAnymailFeatureTests(MailgunBackendMockAPITestCase):
263277
"""Test backend support for Anymail added features"""

0 commit comments

Comments
 (0)