Skip to content

Commit 3e68cba

Browse files
committed
Add retry logic for streaming timeout errors in Urllib3Fetcher
Co-authored-by: Ishaan Gupta <ishaankone@gmail.com> Signed-off-by: Hemang Sharrma <hemangsharrma@gmail.com>
1 parent 590a3b5 commit 3e68cba

File tree

1 file changed

+68
-1
lines changed

1 file changed

+68
-1
lines changed

tuf/ngclient/urllib3_fetcher.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
# Imports
1414
import urllib3
15+
from urllib3.util.retry import Retry
1516

1617
import tuf
1718
from tuf.api import exceptions
@@ -50,7 +51,19 @@ def __init__(
5051
if app_user_agent is not None:
5152
ua = f"{app_user_agent} {ua}"
5253

53-
self._proxy_env = ProxyEnvironment(headers={"User-Agent": ua})
54+
# Configure retry strategy: retry on read timeouts and connection errors
55+
# This enables retries for streaming failures, not just initial connection
56+
retry_strategy = Retry(
57+
total=3,
58+
read=3,
59+
connect=3,
60+
status_forcelist=[500, 502, 503, 504],
61+
raise_on_status=False,
62+
)
63+
64+
self._proxy_env = ProxyEnvironment(
65+
headers={"User-Agent": ua}, retries=retry_strategy
66+
)
5467

5568
def _fetch(self, url: str) -> Iterator[bytes]:
5669
"""Fetch the contents of HTTP/HTTPS url from a remote server.
@@ -82,6 +95,7 @@ def _fetch(self, url: str) -> Iterator[bytes]:
8295
except urllib3.exceptions.MaxRetryError as e:
8396
if isinstance(e.reason, urllib3.exceptions.TimeoutError):
8497
raise exceptions.SlowRetrievalError from e
98+
raise
8599

86100
if response.status >= 400:
87101
response.close()
@@ -106,6 +120,59 @@ def _chunks(
106120
except urllib3.exceptions.MaxRetryError as e:
107121
if isinstance(e.reason, urllib3.exceptions.TimeoutError):
108122
raise exceptions.SlowRetrievalError from e
123+
raise
124+
except (
125+
urllib3.exceptions.ReadTimeoutError,
126+
urllib3.exceptions.ProtocolError,
127+
) as e:
128+
raise exceptions.SlowRetrievalError from e
109129

110130
finally:
111131
response.release_conn()
132+
133+
def download_bytes(self, url: str, max_length: int) -> bytes:
134+
"""Download bytes from given ``url`` with retry on streaming failures.
135+
136+
This override adds retry logic for mid-stream timeout and connection
137+
errors that are not automatically retried by urllib3.
138+
139+
Args:
140+
url: URL string that represents the location of the file.
141+
max_length: Upper bound of data size in bytes.
142+
143+
Raises:
144+
exceptions.DownloadError: An error occurred during download.
145+
exceptions.DownloadLengthMismatchError: Downloaded bytes exceed
146+
``max_length``.
147+
exceptions.DownloadHTTPError: An HTTP error code was received.
148+
149+
Returns:
150+
Content of the file in bytes.
151+
"""
152+
max_retries = 3
153+
last_exception: Exception | None = None
154+
155+
for attempt in range(max_retries):
156+
try:
157+
return super().download_bytes(url, max_length)
158+
except exceptions.SlowRetrievalError as e:
159+
last_exception = e
160+
if attempt < max_retries - 1:
161+
logger.debug(
162+
"Retrying download after streaming error "
163+
"(attempt %d/%d): %s",
164+
attempt + 1,
165+
max_retries,
166+
url,
167+
)
168+
continue
169+
raise
170+
except (
171+
exceptions.DownloadHTTPError,
172+
exceptions.DownloadLengthMismatchError,
173+
):
174+
raise
175+
176+
if last_exception:
177+
raise last_exception
178+
raise exceptions.DownloadError(f"Failed to download {url}")

0 commit comments

Comments
 (0)