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
This commit is contained in:
medmunds
2016-06-01 10:18:27 -07:00
parent 5d2bc66190
commit 1ea9ab6fee
3 changed files with 34 additions and 2 deletions

View File

@@ -65,7 +65,15 @@ class AnymailRequestsBackend(AnymailBaseBackend):
Can raise AnymailRequestsAPIError for HTTP errors in the post Can raise AnymailRequestsAPIError for HTTP errors in the post
""" """
params = payload.get_request_params(self.api_url) 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) self.raise_for_status(response, payload, message)
return response return response

View File

@@ -1,4 +1,5 @@
import json import json
from traceback import format_exception_only
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
from requests import HTTPError from requests import HTTPError
@@ -18,11 +19,13 @@ class AnymailError(Exception):
status_code: HTTP status code of response to ESP send call status_code: HTTP status code of response to ESP send call
payload: data arg (*not* json-stringified) for the ESP send call payload: data arg (*not* json-stringified) for the ESP send call
response: requests.Response from the send call response: requests.Response from the send call
raised_from: original/wrapped Exception
""" """
self.backend = kwargs.pop('backend', None) self.backend = kwargs.pop('backend', None)
self.email_message = kwargs.pop('email_message', None) self.email_message = kwargs.pop('email_message', None)
self.payload = kwargs.pop('payload', None) self.payload = kwargs.pop('payload', None)
self.status_code = kwargs.pop('status_code', None) self.status_code = kwargs.pop('status_code', None)
self.raised_from = kwargs.pop('raised_from', None)
if isinstance(self, HTTPError): if isinstance(self, HTTPError):
# must leave response in kwargs for HTTPError # must leave response in kwargs for HTTPError
self.response = kwargs.get('response', None) self.response = kwargs.get('response', None)
@@ -33,6 +36,7 @@ class AnymailError(Exception):
def __str__(self): def __str__(self):
parts = [ parts = [
" ".join([str(arg) for arg in self.args]), " ".join([str(arg) for arg in self.args]),
self.describe_raised_from(),
self.describe_send(), self.describe_send(),
self.describe_response(), self.describe_response(),
] ]
@@ -68,6 +72,12 @@ class AnymailError(Exception):
pass pass
return description 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): class AnymailAPIError(AnymailError):
"""Exception for unsuccessful response from ESP's API.""" """Exception for unsuccessful response from ESP's API."""

View File

@@ -12,7 +12,7 @@ from django.test import SimpleTestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils.timezone import get_fixed_timezone, override as override_current_timezone 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 anymail.message import attach_inline_image_file
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin
@@ -258,6 +258,20 @@ class MailgunBackendStandardEmailTests(MailgunBackendMockAPITestCase):
with self.assertRaises(AnymailAPIError): with self.assertRaises(AnymailAPIError):
self.message.send() 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): class MailgunBackendAnymailFeatureTests(MailgunBackendMockAPITestCase):
"""Test backend support for Anymail added features""" """Test backend support for Anymail added features"""