Skip to content

Commit 0e90586

Browse files
Validate max_retry_after at construction
Round out the constructor validation: rate and burst already raise on out-of-range values, but max_retry_after did not, so a nan/negative value would silently misbehave in the Retry-After clamp. Reject anything that is not None or a non-negative finite number. Clarify the docs to distinguish this config validation from the (separate) handling of hostile server-sent Retry-After values.
1 parent b0a1b39 commit 0e90586

3 files changed

Lines changed: 32 additions & 7 deletions

File tree

‎aiohttp_client_middlewares/rate_limit.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,13 @@ class RateLimitMiddleware:
118118
numeric ``Retry-After`` header on an HTTP 429 response before returning
119119
it to the caller.
120120
:param max_retry_after: Upper bound, in seconds, on how long a
121-
``Retry-After`` header may make the client sleep. ``None`` removes the
122-
cap. Non-finite (``inf``/``nan``) and non-positive values are always
121+
``Retry-After`` header may make the client sleep. Must be ``None`` (no
122+
cap) or a non-negative, finite number. A server-sent ``Retry-After``
123+
that is itself non-finite (``inf``/``nan``) or non-positive is always
123124
ignored, so a hostile server cannot stall the client indefinitely.
124125
:type max_retry_after: float or None
126+
:raises ValueError: if ``rate``, ``burst`` or ``max_retry_after`` is out of
127+
range.
125128
"""
126129

127130
rate: float
@@ -148,6 +151,13 @@ def __init__(
148151
self._domain_buckets: dict[str, TokenBucket] = defaultdict(
149152
lambda: TokenBucket(rate, burst)
150153
)
154+
if max_retry_after is not None and (
155+
not math.isfinite(max_retry_after) or max_retry_after < 0
156+
):
157+
raise ValueError(
158+
"max_retry_after must be None or a non-negative finite "
159+
f"number, got {max_retry_after!r}"
160+
)
151161

152162
def _get_bucket(self, request: ClientRequest) -> TokenBucket:
153163
if self.per_domain:

‎docs/api.rst‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,19 @@ Rate limiting
9595
``Retry-After`` header on an HTTP 429 response before returning it to the
9696
caller.
9797
:param max_retry_after: Upper bound, in seconds, on how long a ``Retry-After``
98-
header may make the client sleep. ``None`` removes the cap. Non-finite
99-
(``inf``/``nan``) and non-positive values are always ignored, so a hostile
100-
server cannot stall the client indefinitely.
98+
header may make the client sleep. Must be ``None`` (no cap) or a
99+
non-negative, finite number. A server-sent ``Retry-After`` that is itself
100+
non-finite (``inf``/``nan``) or non-positive is always ignored, so a
101+
hostile server cannot stall the client indefinitely.
101102
:type max_retry_after: float or None
102103

103104
The middleware delays each request until the bucket grants it a slot, so the
104105
client never sends faster than ``rate`` requests per second while still
105106
allowing short bursts of up to ``burst`` requests. Slots are served in strict
106107
FIFO order.
107108

108-
``rate`` and ``burst`` are validated on construction and raise
109-
:exc:`ValueError` if out of range.
109+
``rate``, ``burst`` and ``max_retry_after`` are validated on construction and
110+
raise :exc:`ValueError` if out of range.
110111

111112
**Usage**
112113

‎tests/test_rate_limit.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,20 @@ def test_invalid_burst_raises(burst: int) -> None:
224224
RateLimitMiddleware(burst=burst)
225225

226226

227+
@pytest.mark.parametrize("value", [float("nan"), float("inf"), float("-inf"), -1.0])
228+
def test_invalid_max_retry_after_raises(value: float) -> None:
229+
"""A non-finite or negative max_retry_after is rejected at construction."""
230+
with pytest.raises(ValueError, match="max_retry_after"):
231+
RateLimitMiddleware(max_retry_after=value)
232+
233+
234+
@pytest.mark.parametrize("value", [None, 0.0, 0.5, 300.0])
235+
def test_valid_max_retry_after_accepted(value: "float | None") -> None:
236+
"""None and any non-negative finite max_retry_after are accepted."""
237+
middleware = RateLimitMiddleware(max_retry_after=value)
238+
assert middleware.max_retry_after == value
239+
240+
227241
# --- Retry-After edge cases -------------------------------------------------
228242

229243

0 commit comments

Comments
 (0)