Skip to content

Commit c8e7b71

Browse files
committed
fix: respect retry-after header with 429 responses
1 parent 5225929 commit c8e7b71

File tree

4 files changed

+41
-2
lines changed

4 files changed

+41
-2
lines changed

src/poetry/publishing/uploader.py

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def adapter(self) -> adapters.HTTPAdapter:
6969
connect=5,
7070
total=10,
7171
allowed_methods=["GET"],
72+
respect_retry_after_header=True,
7273
status_forcelist=STATUS_FORCELIST,
7374
)
7475

src/poetry/utils/authenticator.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from poetry.config.config import Config
2525
from poetry.exceptions import PoetryException
2626
from poetry.utils.constants import REQUESTS_TIMEOUT
27+
from poetry.utils.constants import RETRY_AFTER_HEADER
2728
from poetry.utils.constants import STATUS_FORCELIST
2829
from poetry.utils.password_manager import HTTPAuthCredential
2930
from poetry.utils.password_manager import PasswordManager
@@ -251,6 +252,7 @@ def request(
251252
send_kwargs.update(settings)
252253

253254
attempt = 0
255+
resp = None
254256

255257
while True:
256258
is_last_attempt = attempt >= 5
@@ -267,14 +269,22 @@ def request(
267269

268270
if not is_last_attempt:
269271
attempt += 1
270-
delay = 0.5 * attempt
272+
delay = self._get_backoff(resp, attempt)
271273
logger.debug("Retrying HTTP request in %s seconds.", delay)
272274
time.sleep(delay)
273275
continue
274276

275277
# this should never really be hit under any sane circumstance
276278
raise PoetryException("Failed HTTP {} request", method.upper())
277279

280+
def _get_backoff(self, response: requests.Response | None, attempt: int) -> float:
281+
if response is not None:
282+
retry_after = response.headers.get(RETRY_AFTER_HEADER, "")
283+
if retry_after:
284+
return float(retry_after)
285+
286+
return 0.5 * attempt
287+
278288
def get(self, url: str, **kwargs: Any) -> requests.Response:
279289
return self.request("get", url, **kwargs)
280290

src/poetry/utils/constants.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@
44
# Timeout for HTTP requests using the requests library.
55
REQUESTS_TIMEOUT = 15
66

7+
RETRY_AFTER_HEADER = "retry-after"
8+
79
# Server response codes to retry requests on.
8-
STATUS_FORCELIST = [500, 501, 502, 503, 504]
10+
STATUS_FORCELIST = [429, 500, 501, 502, 503, 504]

tests/utils/test_authenticator.py

+26
Original file line numberDiff line numberDiff line change
@@ -242,13 +242,39 @@ def callback(*_: Any, **___: Any) -> None:
242242
assert sleep.call_count == 5
243243

244244

245+
def test_authenticator_request_respects_retry_header(
246+
mocker: MockerFixture,
247+
config: Config,
248+
http: type[httpretty.httpretty],
249+
):
250+
sleep = mocker.patch("time.sleep")
251+
sdist_uri = f"https://foo.bar/files/{uuid.uuid4()!s}/foo-0.1.0.tar.gz"
252+
253+
def callback(
254+
request: requests.Request, uri: str, response_headers: dict
255+
) -> list[int | dict | str]:
256+
return [429, {"Retry-After": "42"}, "Retry later"]
257+
258+
http.register_uri(httpretty.GET, sdist_uri, body=callback)
259+
authenticator = Authenticator(config, NullIO())
260+
261+
with pytest.raises(requests.exceptions.HTTPError) as excinfo:
262+
authenticator.request("get", sdist_uri)
263+
264+
assert excinfo.value.response.status_code == 429
265+
assert excinfo.value.response.text == "Retry later"
266+
267+
assert sleep.call_args[0] == (42.0,)
268+
269+
245270
@pytest.mark.parametrize(
246271
["status", "attempts"],
247272
[
248273
(400, 0),
249274
(401, 0),
250275
(403, 0),
251276
(404, 0),
277+
(429, 5),
252278
(500, 5),
253279
(501, 5),
254280
(502, 5),

0 commit comments

Comments
 (0)