Skip to content

Commit 09def30

Browse files
committed
Add timeout to all Requests calls
Use a default timeout of 30 seconds for all requests, and add a REQUESTS_TIMEOUT Anymail setting to override. (I'm making a judgement call that this is not a breaking change in the real world, and not bumping the major version. Theoretically, it could affect you if your network somehow takes >30s to connect to your ESP, but eventually succeeds. If so, set REQUESTS_TIMEOUT to None to restore the earlier behavior.) Fixes #80.
1 parent 5fb4695 commit 09def30

File tree

4 files changed

+80
-1
lines changed

4 files changed

+80
-1
lines changed

anymail/backends/base_requests.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# noinspection PyUnresolvedReferences
55
from six.moves.urllib.parse import urljoin
66

7+
from anymail.utils import get_anymail_setting
78
from .base import AnymailBaseBackend, BasePayload
89
from ..exceptions import AnymailRequestsAPIError, AnymailSerializationError
910
from .._version import __version__
@@ -17,6 +18,7 @@ class AnymailRequestsBackend(AnymailBaseBackend):
1718
def __init__(self, api_url, **kwargs):
1819
"""Init options from Django settings"""
1920
self.api_url = api_url
21+
self.timeout = get_anymail_setting('requests_timeout', kwargs=kwargs, default=30)
2022
super(AnymailRequestsBackend, self).__init__(**kwargs)
2123
self.session = None
2224

@@ -65,6 +67,7 @@ def post_to_esp(self, payload, message):
6567
Can raise AnymailRequestsAPIError for HTTP errors in the post
6668
"""
6769
params = payload.get_request_params(self.api_url)
70+
params.setdefault('timeout', self.timeout)
6871
try:
6972
response = self.session.request(**params)
7073
except requests.RequestException as err:

anymail/backends/mailjet.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def _populate_sender_from_template(self):
115115
if template_id and not self.data.get("FromEmail"):
116116
response = self.backend.session.get(
117117
"%sREST/template/%s/detailcontent" % (self.backend.api_url, template_id),
118-
auth=self.auth
118+
auth=self.auth, timeout=self.backend.timeout
119119
)
120120
self.backend.raise_for_status(response, None, self.message)
121121
json_response = self.backend.deserialize_json_response(response, None, self.message)

docs/installation.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,14 @@ This is actually implemented using HTTP basic authorization, and the string is
269269
technically a "username:password" format. But you should *not* use any real
270270
username or password for this shared secret.
271271

272+
273+
.. setting:: ANYMAIL_REQUESTS_TIMEOUT
274+
275+
.. rubric:: REQUESTS_TIMEOUT
276+
277+
.. versionadded:: 1.3
278+
279+
For Requests-based Anymail backends, the timeout value used for all API calls to your ESP.
280+
The default is 30 seconds. You can set to a single float, a 2-tuple of floats for
281+
separate connection and read timeouts, or `None` to disable timeouts (not recommended).
282+
See :ref:`requests:timeouts` in the Requests docs for more information.

tests/test_base_backends.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from django.test import override_settings
2+
3+
from anymail.backends.base_requests import AnymailRequestsBackend, RequestsPayload
4+
from anymail.message import AnymailMessage, AnymailRecipientStatus
5+
6+
from .mock_requests_backend import RequestsBackendMockAPITestCase
7+
8+
9+
class MinimalRequestsBackend(AnymailRequestsBackend):
10+
"""(useful only for these tests)"""
11+
12+
esp_name = "Example"
13+
14+
def __init__(self, **kwargs):
15+
super(MinimalRequestsBackend, self).__init__("https://esp.example.com/api/", **kwargs)
16+
17+
def build_message_payload(self, message, defaults):
18+
return MinimalRequestsPayload(message, defaults, self)
19+
20+
def parse_recipient_status(self, response, payload, message):
21+
return {'[email protected]': AnymailRecipientStatus('message-id', 'sent')}
22+
23+
24+
class MinimalRequestsPayload(RequestsPayload):
25+
def init_payload(self):
26+
pass
27+
28+
def _noop(self, *args, **kwargs):
29+
pass
30+
31+
set_from_email = _noop
32+
set_recipients = _noop
33+
set_subject = _noop
34+
set_reply_to = _noop
35+
set_extra_headers = _noop
36+
set_text_body = _noop
37+
set_html_body = _noop
38+
add_attachment = _noop
39+
40+
41+
@override_settings(EMAIL_BACKEND='tests.test_base_backends.MinimalRequestsBackend')
42+
class RequestsBackendBaseTestCase(RequestsBackendMockAPITestCase):
43+
"""Test common functionality in AnymailRequestsBackend"""
44+
45+
def setUp(self):
46+
super(RequestsBackendBaseTestCase, self).setUp()
47+
self.message = AnymailMessage('Subject', 'Text Body', '[email protected]', ['[email protected]'])
48+
49+
def test_minimal_requests_backend(self):
50+
"""Make sure the testing backend defined above actually works"""
51+
self.message.send()
52+
self.assert_esp_called("https://esp.example.com/api/")
53+
54+
def test_timeout_default(self):
55+
"""All requests have a 30 second default timeout"""
56+
self.message.send()
57+
timeout = self.get_api_call_arg('timeout')
58+
self.assertEqual(timeout, 30)
59+
60+
@override_settings(ANYMAIL_REQUESTS_TIMEOUT=5)
61+
def test_timeout_setting(self):
62+
"""You can use the Anymail setting REQUESTS_TIMEOUT to override the default"""
63+
self.message.send()
64+
timeout = self.get_api_call_arg('timeout')
65+
self.assertEqual(timeout, 5)

0 commit comments

Comments
 (0)