Commit 6b3a6161 by Nejc Habjan Committed by Randy Döring

fix: respect retry-after header with 429 responses

parent fba14ba5
...@@ -69,6 +69,7 @@ class Uploader: ...@@ -69,6 +69,7 @@ class Uploader:
connect=5, connect=5,
total=10, total=10,
allowed_methods=["GET"], allowed_methods=["GET"],
respect_retry_after_header=True,
status_forcelist=STATUS_FORCELIST, status_forcelist=STATUS_FORCELIST,
) )
......
...@@ -24,6 +24,7 @@ from filelock import FileLock ...@@ -24,6 +24,7 @@ from filelock import FileLock
from poetry.config.config import Config from poetry.config.config import Config
from poetry.exceptions import PoetryException from poetry.exceptions import PoetryException
from poetry.utils.constants import REQUESTS_TIMEOUT from poetry.utils.constants import REQUESTS_TIMEOUT
from poetry.utils.constants import RETRY_AFTER_HEADER
from poetry.utils.constants import STATUS_FORCELIST from poetry.utils.constants import STATUS_FORCELIST
from poetry.utils.password_manager import HTTPAuthCredential from poetry.utils.password_manager import HTTPAuthCredential
from poetry.utils.password_manager import PasswordManager from poetry.utils.password_manager import PasswordManager
...@@ -251,6 +252,7 @@ class Authenticator: ...@@ -251,6 +252,7 @@ class Authenticator:
send_kwargs.update(settings) send_kwargs.update(settings)
attempt = 0 attempt = 0
resp = None
while True: while True:
is_last_attempt = attempt >= 5 is_last_attempt = attempt >= 5
...@@ -267,7 +269,7 @@ class Authenticator: ...@@ -267,7 +269,7 @@ class Authenticator:
if not is_last_attempt: if not is_last_attempt:
attempt += 1 attempt += 1
delay = 0.5 * attempt delay = self._get_backoff(resp, attempt)
logger.debug("Retrying HTTP request in %s seconds.", delay) logger.debug("Retrying HTTP request in %s seconds.", delay)
time.sleep(delay) time.sleep(delay)
continue continue
...@@ -275,6 +277,14 @@ class Authenticator: ...@@ -275,6 +277,14 @@ class Authenticator:
# this should never really be hit under any sane circumstance # this should never really be hit under any sane circumstance
raise PoetryException("Failed HTTP {} request", method.upper()) raise PoetryException("Failed HTTP {} request", method.upper())
def _get_backoff(self, response: requests.Response | None, attempt: int) -> float:
if response is not None:
retry_after = response.headers.get(RETRY_AFTER_HEADER, "")
if retry_after:
return float(retry_after)
return 0.5 * attempt
def get(self, url: str, **kwargs: Any) -> requests.Response: def get(self, url: str, **kwargs: Any) -> requests.Response:
return self.request("get", url, **kwargs) return self.request("get", url, **kwargs)
......
...@@ -4,5 +4,7 @@ from __future__ import annotations ...@@ -4,5 +4,7 @@ from __future__ import annotations
# Timeout for HTTP requests using the requests library. # Timeout for HTTP requests using the requests library.
REQUESTS_TIMEOUT = 15 REQUESTS_TIMEOUT = 15
RETRY_AFTER_HEADER = "retry-after"
# Server response codes to retry requests on. # Server response codes to retry requests on.
STATUS_FORCELIST = [500, 501, 502, 503, 504] STATUS_FORCELIST = [429, 500, 501, 502, 503, 504]
...@@ -242,6 +242,33 @@ def test_authenticator_request_raises_exception_when_attempts_exhausted( ...@@ -242,6 +242,33 @@ def test_authenticator_request_raises_exception_when_attempts_exhausted(
assert sleep.call_count == 5 assert sleep.call_count == 5
def test_authenticator_request_respects_retry_header(
mocker: MockerFixture,
config: Config,
http: type[httpretty.httpretty],
):
sleep = mocker.patch("time.sleep")
sdist_uri = f"https://foo.bar/files/{uuid.uuid4()!s}/foo-0.1.0.tar.gz"
content = str(uuid.uuid4())
seen = []
def callback(
request: requests.Request, uri: str, response_headers: dict
) -> list[int | dict | str]:
if not seen.count(uri):
seen.append(uri)
return [429, {"Retry-After": "42"}, "Retry later"]
return [200, response_headers, content]
http.register_uri(httpretty.GET, sdist_uri, body=callback)
authenticator = Authenticator(config, NullIO())
response = authenticator.request("get", sdist_uri)
assert sleep.call_args[0] == (42.0,)
assert response.text == content
@pytest.mark.parametrize( @pytest.mark.parametrize(
["status", "attempts"], ["status", "attempts"],
[ [
...@@ -249,6 +276,7 @@ def test_authenticator_request_raises_exception_when_attempts_exhausted( ...@@ -249,6 +276,7 @@ def test_authenticator_request_raises_exception_when_attempts_exhausted(
(401, 0), (401, 0),
(403, 0), (403, 0),
(404, 0), (404, 0),
(429, 5),
(500, 5), (500, 5),
(501, 5), (501, 5),
(502, 5), (502, 5),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment