Skip to content

Commit

Permalink
fix: respect retry-after header with 429 responses
Browse files Browse the repository at this point in the history
  • Loading branch information
nejch committed Nov 22, 2022
1 parent 4097014 commit 02ffff8
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/poetry/publishing/uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def adapter(self) -> adapters.HTTPAdapter:
connect=5,
total=10,
allowed_methods=["GET"],
respect_retry_after_header=True,
status_forcelist=STATUS_FORCELIST,
)

Expand Down
12 changes: 11 additions & 1 deletion src/poetry/utils/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from poetry.config.config import Config
from poetry.exceptions import PoetryException
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.password_manager import HTTPAuthCredential
from poetry.utils.password_manager import PasswordManager
Expand Down Expand Up @@ -251,6 +252,7 @@ def request(
send_kwargs.update(settings)

attempt = 0
resp = None

while True:
is_last_attempt = attempt >= 5
Expand All @@ -267,14 +269,22 @@ def request(

if not is_last_attempt:
attempt += 1
delay = 0.5 * attempt
delay = self._get_backoff(resp, attempt)
logger.debug("Retrying HTTP request in %s seconds.", delay)
time.sleep(delay)
continue

# this should never really be hit under any sane circumstance
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:
return self.request("get", url, **kwargs)

Expand Down
4 changes: 3 additions & 1 deletion src/poetry/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
# Timeout for HTTP requests using the requests library.
REQUESTS_TIMEOUT = 15

RETRY_AFTER_HEADER = "retry-after"

# Server response codes to retry requests on.
STATUS_FORCELIST = [500, 501, 502, 503, 504]
STATUS_FORCELIST = [429, 500, 501, 502, 503, 504]
26 changes: 26 additions & 0 deletions tests/utils/test_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,39 @@ def callback(*_: Any, **___: Any) -> None:
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"

def callback(
request: requests.Request, uri: str, response_headers: dict
) -> list[int | dict | str]:
return [429, {"Retry-After": "42"}, "Retry later"]

http.register_uri(httpretty.GET, sdist_uri, body=callback)
authenticator = Authenticator(config, NullIO())

with pytest.raises(requests.exceptions.HTTPError) as excinfo:
authenticator.request("get", sdist_uri)

assert excinfo.value.response.status_code == 429
assert excinfo.value.response.text == "Retry later"

assert sleep.call_args[0] == (42.0,)


@pytest.mark.parametrize(
["status", "attempts"],
[
(400, 0),
(401, 0),
(403, 0),
(404, 0),
(429, 5),
(500, 5),
(501, 5),
(502, 5),
Expand Down

0 comments on commit 02ffff8

Please sign in to comment.