Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Commit 74d6b54

Browse files
authored
Merge pull request #118 from RatanShreshtha/merge-from-master-2019-06-05
Merge from master (Jun 05th, 2019)
2 parents 8bc89db + c68b81d commit 74d6b54

File tree

6 files changed

+94
-11
lines changed

6 files changed

+94
-11
lines changed

CHANGES.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ Upcoming 2.0 Changes
2424
useful to persist them up to the user.
2525

2626
* Removed ``BodyNotHttplibCompatible`` and ``ResponseNotChunked`` exceptions.
27+
dev (master)
28+
------------
29+
30+
* Propagate Retry-After header settings to subsequent retries. (Pull #1607)
31+
32+
* Fix edge case where Retry-After header was still respected even when
33+
explicitly opted out of. (Pull #1607)
34+
2735

2836
1.25.3 (2019-05-23)
2937
-------------------

CONTRIBUTORS.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,5 +275,8 @@ In chronological order:
275275
* Katsuhiko YOSHIDA <https://github.com/kyoshidajp>
276276
* Remove Authorization header regardless of case when redirecting to cross-site
277277

278+
* James Meickle <https://permadeath.com/>
279+
* Improve handling of Retry-After header
280+
278281
* [Your name or handle] <[email or website]>
279282
* [Brief summary of your changes]

docs/user-guide.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,8 @@ to the standard-library :mod:`ssl` module. You may experience
302302
Using timeouts
303303
--------------
304304

305-
Timeouts allow you to control how long requests are allowed to run before
306-
being aborted. In simple cases, you can specify a timeout as a ``float``
305+
Timeouts allow you to control how long (in seconds) requests are allowed to run
306+
before being aborted. In simple cases, you can specify a timeout as a ``float``
307307
to :meth:`~poolmanager.PoolManager.request`::
308308

309309
>>> http.request(

src/urllib3/util/retry.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ def new(self, **kw):
210210
raise_on_status=self.raise_on_status,
211211
history=self.history,
212212
remove_headers_on_redirect=self.remove_headers_on_redirect,
213+
respect_retry_after_header=self.respect_retry_after_header,
213214
)
214215
params.update(kw)
215216
return type(self)(**params)
@@ -294,7 +295,7 @@ def sleep(self, response=None):
294295
this method will return immediately.
295296
"""
296297

297-
if response:
298+
if self.respect_retry_after_header and response:
298299
slept = self.sleep_for_retry(response)
299300
if slept:
300301
return

src/urllib3/util/timeout.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,20 @@ class Timeout(object):
4646
:type total: integer, float, or None
4747
4848
:param connect:
49-
The maximum amount of time to wait for a connection attempt to a server
50-
to succeed. Omitting the parameter will default the connect timeout to
51-
the system default, probably `the global default timeout in socket.py
49+
The maximum amount of time (in seconds) to wait for a connection
50+
attempt to a server to succeed. Omitting the parameter will default the
51+
connect timeout to the system default, probably `the global default
52+
timeout in socket.py
5253
<http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_.
5354
None will set an infinite timeout for connection attempts.
5455
5556
:type connect: integer, float, or None
5657
5758
:param read:
58-
The maximum amount of time to wait between consecutive
59-
read operations for a response from the server. Omitting
60-
the parameter will default the read timeout to the system
61-
default, probably `the global default timeout in socket.py
59+
The maximum amount of time (in seconds) to wait between consecutive
60+
read operations for a response from the server. Omitting the parameter
61+
will default the read timeout to the system default, probably `the
62+
global default timeout in socket.py
6263
<http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_.
6364
None will set an infinite timeout.
6465
@@ -195,7 +196,7 @@ def start_connect(self):
195196
def get_connect_duration(self):
196197
""" Gets the time elapsed since the call to :meth:`start_connect`.
197198
198-
:return: Elapsed time.
199+
:return: Elapsed time in seconds.
199200
:rtype: float
200201
:raises urllib3.exceptions.TimeoutStateError: if you attempt
201202
to get duration for a timer that hasn't been started.

test/test_retry.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import datetime
2+
import mock
13
import pytest
4+
import time
25

36
from urllib3.response import HTTPResponse
7+
from urllib3.packages import six
48
from urllib3.packages.six.moves import xrange
59
from urllib3.util.retry import Retry, RequestHistory
610
from urllib3.exceptions import (
711
ConnectTimeoutError,
12+
InvalidHeader,
813
MaxRetryError,
914
ReadTimeoutError,
1015
ResponseError,
@@ -271,3 +276,68 @@ def test_retry_set_remove_headers_on_redirect(self):
271276
retry = Retry(remove_headers_on_redirect=["X-API-Secret"])
272277

273278
assert list(retry.remove_headers_on_redirect) == ["x-api-secret"]
279+
280+
@pytest.mark.parametrize("value", ["-1", "+1", "1.0", six.u("\xb2")]) # \xb2 = ^2
281+
def test_parse_retry_after_invalid(self, value):
282+
retry = Retry()
283+
with pytest.raises(InvalidHeader):
284+
retry.parse_retry_after(value)
285+
286+
@pytest.mark.parametrize(
287+
"value, expected", [("0", 0), ("1000", 1000), ("\t42 ", 42)]
288+
)
289+
def test_parse_retry_after(self, value, expected):
290+
retry = Retry()
291+
assert retry.parse_retry_after(value) == expected
292+
293+
@pytest.mark.parametrize("respect_retry_after_header", [True, False])
294+
def test_respect_retry_after_header_propagated(self, respect_retry_after_header):
295+
296+
retry = Retry(respect_retry_after_header=respect_retry_after_header)
297+
new_retry = retry.new()
298+
assert new_retry.respect_retry_after_header == respect_retry_after_header
299+
300+
@pytest.mark.parametrize(
301+
"retry_after_header,respect_retry_after_header,sleep_duration",
302+
[
303+
("3600", True, 3600),
304+
("3600", False, None),
305+
# Will sleep due to header is 1 hour in future
306+
("Mon, 3 Jun 2019 12:00:00 UTC", True, 3600),
307+
# Won't sleep due to not respecting header
308+
("Mon, 3 Jun 2019 12:00:00 UTC", False, None),
309+
# Won't sleep due to current time reached
310+
("Mon, 3 Jun 2019 11:00:00 UTC", True, None),
311+
# Won't sleep due to current time reached + not respecting header
312+
("Mon, 3 Jun 2019 11:00:00 UTC", False, None),
313+
],
314+
)
315+
def test_respect_retry_after_header_sleep(
316+
self, retry_after_header, respect_retry_after_header, sleep_duration
317+
):
318+
retry = Retry(respect_retry_after_header=respect_retry_after_header)
319+
320+
# Date header syntax can specify an absolute date; compare this to the
321+
# time in the parametrized inputs above.
322+
current_time = mock.MagicMock(
323+
return_value=time.mktime(
324+
datetime.datetime(year=2019, month=6, day=3, hour=11).timetuple()
325+
)
326+
)
327+
328+
with mock.patch("time.sleep") as sleep_mock, mock.patch(
329+
"time.time", current_time
330+
):
331+
# for the default behavior, it must be in RETRY_AFTER_STATUS_CODES
332+
response = HTTPResponse(
333+
status=503, headers={"Retry-After": retry_after_header}
334+
)
335+
336+
retry.sleep(response)
337+
338+
# The expected behavior is that we'll only sleep if respecting
339+
# this header (since we won't have any backoff sleep attempts)
340+
if respect_retry_after_header and sleep_duration is not None:
341+
sleep_mock.assert_called_with(sleep_duration)
342+
else:
343+
sleep_mock.assert_not_called()

0 commit comments

Comments
 (0)