Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Retry class to determine to retry based on response #2500

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/1445.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `is_response_retry` variation of `Retry.is_retry` that considers `HTTPResponse`.
EnricoMi marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 1 addition & 2 deletions src/urllib3/connectionpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,8 +932,7 @@ def urlopen( # type: ignore[override]
)

# Check if we should retry the HTTP response.
has_retry_after = bool(response.headers.get("Retry-After"))
if retries.is_retry(method, response.status, has_retry_after):
if retries.is_response_retry(method, response):
try:
retries = retries.increment(method, url, response=response, _pool=self)
except MaxRetryError:
Expand Down
10 changes: 10 additions & 0 deletions src/urllib3/util/retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,16 @@ def _is_method_retryable(self, method: str) -> bool:
return False
return True

def is_response_retry(self, method: str, response: BaseHTTPResponse) -> bool:
"""Is this method/response retryable? (Based on allowlists and control
variables such as the number of total retries to allow, whether to
respect the Retry-After header, whether this header is present, and
whether the returned status code is on the list of status codes to
be retried upon on the presence of the aforementioned header)
"""
has_retry_after = bool(response.headers.get("Retry-After"))
return self.is_retry(method, response.status, has_retry_after)

def is_retry(
self, method: str, status_code: int, has_retry_after: bool = False
) -> bool:
Expand Down
15 changes: 15 additions & 0 deletions test/test_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,27 +239,37 @@ def test_sleep(self) -> None:
def test_status_forcelist(self) -> None:
retry = Retry(status_forcelist=range(500, 600))
assert not retry.is_retry("GET", status_code=200)
assert not retry.is_response_retry("GET", HTTPResponse(status=200))
assert not retry.is_retry("GET", status_code=400)
assert not retry.is_response_retry("GET", HTTPResponse(status=400))
assert retry.is_retry("GET", status_code=500)
assert retry.is_response_retry("GET", HTTPResponse(status=500))

retry = Retry(total=1, status_forcelist=[418])
assert not retry.is_retry("GET", status_code=400)
assert not retry.is_response_retry("GET", HTTPResponse(status=400))
assert retry.is_retry("GET", status_code=418)
assert retry.is_response_retry("GET", HTTPResponse(status=418))

# String status codes are not matched.
retry = Retry(total=1, status_forcelist=["418"]) # type: ignore[list-item]
assert not retry.is_retry("GET", status_code=418)
assert not retry.is_response_retry("GET", HTTPResponse(status=418))

def test_allowed_methods_with_status_forcelist(self) -> None:
# Falsey allowed_methods means to retry on any method.
retry = Retry(status_forcelist=[500], allowed_methods=None)
assert retry.is_retry("GET", status_code=500)
assert retry.is_response_retry("GET", HTTPResponse(status=500))
assert retry.is_retry("POST", status_code=500)
assert retry.is_response_retry("POST", HTTPResponse(status=500))

# Criteria of allowed_methods and status_forcelist are ANDed.
retry = Retry(status_forcelist=[500], allowed_methods=["POST"])
assert not retry.is_retry("GET", status_code=500)
assert not retry.is_response_retry("GET", HTTPResponse(status=500))
assert retry.is_retry("POST", status_code=500)
assert retry.is_response_retry("POST", HTTPResponse(status=500))

def test_exhausted(self) -> None:
assert not Retry(0).is_exhausted()
Expand Down Expand Up @@ -411,6 +421,11 @@ def test_respect_retry_after_header_sleep(
status=503, headers={"Retry-After": retry_after_header}
)

# for the default behaviour, method must be in DEFAULT_ALLOWED_METHODS
assert (
retry.is_response_retry("GET", response) == respect_retry_after_header
)

retry.sleep(response)

# The expected behavior is that we'll only sleep if respecting
Expand Down