Skip to content

Commit

Permalink
Upgrade requests exceptions to AnymailRequestsAPIError
Browse files Browse the repository at this point in the history
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
  • Loading branch information
medmunds committed Jun 1, 2016
1 parent 5d2bc66 commit 1ea9ab6
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 2 deletions.
10 changes: 9 additions & 1 deletion anymail/backends/base_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,15 @@ def post_to_esp(self, payload, message):
Can raise AnymailRequestsAPIError for HTTP errors in the post
"""
params = payload.get_request_params(self.api_url)
response = self.session.request(**params)
try:
response = self.session.request(**params)
except requests.RequestException as err:
# raise an exception that is both AnymailRequestsAPIError
# and the original requests exception type
exc_class = type('AnymailRequestsAPIError', (AnymailRequestsAPIError, type(err)), {})
raise exc_class(
"Error posting to %s:" % params.get('url', '<missing url>'),
raised_from=err, email_message=message, payload=payload)
self.raise_for_status(response, payload, message)
return response

Expand Down
10 changes: 10 additions & 0 deletions anymail/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from traceback import format_exception_only

from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
from requests import HTTPError
Expand All @@ -18,11 +19,13 @@ def __init__(self, *args, **kwargs):
status_code: HTTP status code of response to ESP send call
payload: data arg (*not* json-stringified) for the ESP send call
response: requests.Response from the send call
raised_from: original/wrapped Exception
"""
self.backend = kwargs.pop('backend', None)
self.email_message = kwargs.pop('email_message', None)
self.payload = kwargs.pop('payload', None)
self.status_code = kwargs.pop('status_code', None)
self.raised_from = kwargs.pop('raised_from', None)
if isinstance(self, HTTPError):
# must leave response in kwargs for HTTPError
self.response = kwargs.get('response', None)
Expand All @@ -33,6 +36,7 @@ def __init__(self, *args, **kwargs):
def __str__(self):
parts = [
" ".join([str(arg) for arg in self.args]),
self.describe_raised_from(),
self.describe_send(),
self.describe_response(),
]
Expand Down Expand Up @@ -68,6 +72,12 @@ def describe_response(self):
pass
return description

def describe_raised_from(self):
"""Return the original exception"""
if self.raised_from is None:
return None
return ''.join(format_exception_only(type(self.raised_from), self.raised_from)).strip()


class AnymailAPIError(AnymailError):
"""Exception for unsuccessful response from ESP's API."""
Expand Down
16 changes: 15 additions & 1 deletion tests/test_mailgun_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from django.test.utils import override_settings
from django.utils.timezone import get_fixed_timezone, override as override_current_timezone

from anymail.exceptions import AnymailAPIError, AnymailUnsupportedFeature
from anymail.exceptions import AnymailAPIError, AnymailRequestsAPIError, AnymailUnsupportedFeature
from anymail.message import attach_inline_image_file

from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin
Expand Down Expand Up @@ -258,6 +258,20 @@ def test_api_error_includes_details(self):
with self.assertRaises(AnymailAPIError):
self.message.send()

def test_requests_exception(self):
"""Exception during API call should be AnymailAPIError"""
# (The post itself raises an error -- different from returning a failure response)
from requests.exceptions import SSLError # a low-level requests exception
self.mock_request.side_effect = SSLError("Something bad")
with self.assertRaisesMessage(AnymailRequestsAPIError, "Something bad") as cm:
self.message.send()
self.assertIsInstance(cm.exception, SSLError) # also retains specific requests exception class

# Make sure fail_silently is respected
self.mock_request.side_effect = SSLError("Something bad")
sent = mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com'], fail_silently=True)
self.assertEqual(sent, 0)


class MailgunBackendAnymailFeatureTests(MailgunBackendMockAPITestCase):
"""Test backend support for Anymail added features"""
Expand Down

0 comments on commit 1ea9ab6

Please sign in to comment.