返回 last30days-skill
test_reddit_ratelimit_reset.py
根目录 / tests / test_reddit_ratelimit_reset.py
1 """Reddit answers an anonymous 429 with x-ratelimit-reset and no Retry-After.
2
3 Reading only Retry-After sends the caller into exponential backoff (3s, 5s, 9s),
4 every step of which is shorter than the ~42s Reddit actually requires, so each
5 retry re-429s and the source is reported dead when it was only early.
6 """
7
8 from lib import http
9
10
11 class TestRetryDelayFromHeaders:
12 def test_prefers_retry_after_when_present(self):
13 assert http.retry_delay_from_headers({"Retry-After": "17"}, 3.0) == 17.0
14
15 def test_falls_back_to_x_ratelimit_reset(self):
16 # The header Reddit actually sends on search/RSS 429s.
17 assert http.retry_delay_from_headers({"x-ratelimit-reset": "42"}, 3.0) == 42.0
18
19 def test_retry_after_wins_over_ratelimit_reset(self):
20 headers = {"Retry-After": "5", "x-ratelimit-reset": "42"}
21 assert http.retry_delay_from_headers(headers, 3.0) == 5.0
22
23 def test_real_reddit_429_headers(self):
24 # Captured verbatim from an anonymous GET to
25 # https://www.reddit.com/search.rss?q=... on 2026-08-23.
26 headers = {
27 "x-ratelimit-used": "1",
28 "x-ratelimit-remaining": "0.0",
29 "x-ratelimit-reset": "42",
30 "server-timing": 'reddit-ct;desc="dn=FT,p=LON,cs=MISS"',
31 }
32 # Must not fall through to the 3s fallback -- that is the bug.
33 assert http.retry_delay_from_headers(headers, 3.0) == 42.0
34
35 def test_missing_headers_use_fallback(self):
36 assert http.retry_delay_from_headers({}, 7.5) == 7.5
37 assert http.retry_delay_from_headers(None, 7.5) == 7.5
38
39 def test_unparseable_values_use_fallback(self):
40 # Retry-After may be an HTTP-date rather than a delta-seconds integer.
41 assert http.retry_delay_from_headers({"Retry-After": "Wed, 21 Oct 2026 07:28:00 GMT"}, 4.0) == 4.0
42 assert http.retry_delay_from_headers({"x-ratelimit-reset": ""}, 4.0) == 4.0
43
44 def test_non_positive_values_use_fallback(self):
45 # A spent bucket sometimes reports 0; sleeping 0 just re-429s immediately.
46 assert http.retry_delay_from_headers({"x-ratelimit-reset": "0"}, 4.0) == 4.0
47 assert http.retry_delay_from_headers({"Retry-After": "-1"}, 4.0) == 4.0
48
49 def test_skips_unparseable_header_and_reads_the_next(self):
50 headers = {"Retry-After": "soon", "x-ratelimit-reset": "42"}
51 assert http.retry_delay_from_headers(headers, 3.0) == 42.0
52
53
54 class TestRetryDelayBounds:
55 """A reset header must never park a thread for minutes (or, for an epoch
56 timestamp, for years)."""
57
58 def test_epoch_reset_is_converted_to_a_delta_and_capped(self):
59 # GitHub sends x-ratelimit-reset as epoch seconds with no Retry-After.
60 import time
61 epoch = str(int(time.time()) + 30)
62 delay = http.retry_delay_from_headers({"x-ratelimit-reset": epoch}, 3.0)
63 assert 0 < delay <= http.MAX_RETRY_DELAY_SECONDS
64 assert 25 <= delay <= 31
65
66 def test_far_future_epoch_is_capped(self):
67 # 2100-01-01 as epoch seconds: a delta of decades still yields the cap.
68 delay = http.retry_delay_from_headers({"x-ratelimit-reset": "4102444800"}, 3.0)
69 assert delay == http.MAX_RETRY_DELAY_SECONDS
70
71 def test_long_delta_is_capped(self):
72 assert http.retry_delay_from_headers({"x-ratelimit-reset": "540"}, 3.0) == http.MAX_RETRY_DELAY_SECONDS
73 assert http.retry_delay_from_headers({"Retry-After": "600"}, 3.0) == http.MAX_RETRY_DELAY_SECONDS
74
74 lines PYTHON