From 1ea9ab6fee2fbfe9976133d21e64f45a851d601d Mon Sep 17 00:00:00 2001 From: medmunds Date: Wed, 1 Jun 2016 10:18:27 -0700 Subject: [PATCH] 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 --- anymail/backends/base_requests.py | 10 +++++++++- anymail/exceptions.py | 10 ++++++++++ tests/test_mailgun_backend.py | 16 +++++++++++++++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/anymail/backends/base_requests.py b/anymail/backends/base_requests.py index 02f78a9..294ad5f 100644 --- a/anymail/backends/base_requests.py +++ b/anymail/backends/base_requests.py @@ -65,7 +65,15 @@ class AnymailRequestsBackend(AnymailBaseBackend): 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', ''), + raised_from=err, email_message=message, payload=payload) self.raise_for_status(response, payload, message) return response diff --git a/anymail/exceptions.py b/anymail/exceptions.py index 2c765bd..8e55532 100644 --- a/anymail/exceptions.py +++ b/anymail/exceptions.py @@ -1,4 +1,5 @@ import json +from traceback import format_exception_only from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from requests import HTTPError @@ -18,11 +19,13 @@ class AnymailError(Exception): 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) @@ -33,6 +36,7 @@ class AnymailError(Exception): def __str__(self): parts = [ " ".join([str(arg) for arg in self.args]), + self.describe_raised_from(), self.describe_send(), self.describe_response(), ] @@ -68,6 +72,12 @@ class AnymailError(Exception): 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.""" diff --git a/tests/test_mailgun_backend.py b/tests/test_mailgun_backend.py index 2aa4a19..8450942 100644 --- a/tests/test_mailgun_backend.py +++ b/tests/test_mailgun_backend.py @@ -12,7 +12,7 @@ from django.test import SimpleTestCase 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 @@ -258,6 +258,20 @@ class MailgunBackendStandardEmailTests(MailgunBackendMockAPITestCase): 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"""