Skip to content

Commit 13f0bfd

Browse files
authored
Handle massive values in Retry-After when calculating time to sleep for (#3743)
1 parent 8c480bf commit 13f0bfd

3 files changed

Lines changed: 29 additions & 0 deletions

File tree

changelog/3743.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Started treating ``Retry-After`` times greater than 6 hours as 6 hours by default.

src/urllib3/util/retry.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,11 @@ class Retry:
178178
Sequence of headers to remove from the request when a response
179179
indicating a redirect is returned before firing off the redirected
180180
request.
181+
182+
:param int retry_after_max: Number of seconds to allow as the maximum for
183+
Retry-After headers. Defaults to :attr:`Retry.DEFAULT_RETRY_AFTER_MAX`.
184+
Any Retry-After headers larger than this value will be limited to this
185+
value.
181186
"""
182187

183188
#: Default methods to be used for ``allowed_methods``
@@ -196,6 +201,10 @@ class Retry:
196201
#: Default maximum backoff time.
197202
DEFAULT_BACKOFF_MAX = 120
198203

204+
# This is undocumented in the RFC. Setting to 6 hours matches other popular libraries.
205+
#: Default maximum allowed value for Retry-After headers in seconds
206+
DEFAULT_RETRY_AFTER_MAX: typing.Final[int] = 21600
207+
199208
# Backward compatibility; assigned outside of the class.
200209
DEFAULT: typing.ClassVar[Retry]
201210

@@ -219,6 +228,7 @@ def __init__(
219228
str
220229
] = DEFAULT_REMOVE_HEADERS_ON_REDIRECT,
221230
backoff_jitter: float = 0.0,
231+
retry_after_max: int = DEFAULT_RETRY_AFTER_MAX,
222232
) -> None:
223233
self.total = total
224234
self.connect = connect
@@ -235,6 +245,7 @@ def __init__(
235245
self.allowed_methods = allowed_methods
236246
self.backoff_factor = backoff_factor
237247
self.backoff_max = backoff_max
248+
self.retry_after_max = retry_after_max
238249
self.raise_on_redirect = raise_on_redirect
239250
self.raise_on_status = raise_on_status
240251
self.history = history or ()
@@ -256,6 +267,7 @@ def new(self, **kw: typing.Any) -> Self:
256267
status_forcelist=self.status_forcelist,
257268
backoff_factor=self.backoff_factor,
258269
backoff_max=self.backoff_max,
270+
retry_after_max=self.retry_after_max,
259271
raise_on_redirect=self.raise_on_redirect,
260272
raise_on_status=self.raise_on_status,
261273
history=self.history,
@@ -320,6 +332,10 @@ def parse_retry_after(self, retry_after: str) -> float:
320332

321333
seconds = max(seconds, 0)
322334

335+
# Check the seconds do not exceed the specified maximum
336+
if seconds > self.retry_after_max:
337+
seconds = self.retry_after_max
338+
323339
return seconds
324340

325341
def get_retry_after(self, response: BaseHTTPResponse) -> float | None:

test/test_retry.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,18 @@ def test_configurable_backoff_max(self) -> None:
181181
retry = retry.increment(method="GET")
182182
assert retry.get_backoff_time() == max_backoff
183183

184+
def test_configurable_retry_after_max(self) -> None:
185+
"""Configurable retry after is computed correctly"""
186+
max_retry_after = Retry.DEFAULT_RETRY_AFTER_MAX
187+
188+
retry = Retry()
189+
assert retry.parse_retry_after(str(max_retry_after)) == max_retry_after
190+
assert retry.parse_retry_after(str(max_retry_after + 1)) == max_retry_after
191+
192+
retry = Retry(retry_after_max=1)
193+
assert retry.parse_retry_after(str(1)) == 1
194+
assert retry.parse_retry_after(str(2)) == 1
195+
184196
def test_backoff_jitter(self) -> None:
185197
"""Backoff with jitter is computed correctly"""
186198
max_backoff = 1

0 commit comments

Comments
 (0)