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 Apr 2, 2023
1 parent c12661f commit 92933fd
Show file tree
Hide file tree
Showing 4 changed files with 43 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]
28 changes: 28 additions & 0 deletions tests/utils/test_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,41 @@ 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"
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(
["status", "attempts"],
[
(400, 0),
(401, 0),
(403, 0),
(404, 0),
(429, 5),
(500, 5),
(501, 5),
(502, 5),
Expand Down

0 comments on commit 92933fd

Please sign in to comment.