Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions tests/test_fetcher_ng.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,46 @@ def test_response_read_timeout(self, mock_session_get: Mock) -> None:
next(self.fetcher.fetch(self.url))
mock_response.stream.assert_called_once()

# urllib3 raises ReadTimeoutError directly when the gap timeout expires
# mid-stream: it is a TimeoutError but not a MaxRetryError, so it is not
# covered by the MaxRetryError case above.
@patch.object(urllib3.PoolManager, "request")
def test_response_read_timeout_error(self, mock_session_get: Mock) -> None:
mock_response = Mock()
mock_response.status = 200
attr = {
"stream.side_effect": urllib3.exceptions.ReadTimeoutError(
urllib3.connectionpool.ConnectionPool("localhost"),
"",
"Read timed out.",
)
}
mock_response.configure_mock(**attr)
mock_session_get.return_value = mock_response

with self.assertRaises(exceptions.SlowRetrievalError):
next(self.fetcher.fetch(self.url))
mock_response.stream.assert_called_once()

# The public download_* API documents DownloadError: a mid-stream timeout
# must not escape as a raw urllib3 error.
@patch.object(urllib3.PoolManager, "request")
def test_download_bytes_read_timeout(self, mock_session_get: Mock) -> None:
mock_response = Mock()
mock_response.status = 200
attr = {
"stream.side_effect": urllib3.exceptions.ReadTimeoutError(
urllib3.connectionpool.ConnectionPool("localhost"),
"",
"Read timed out.",
)
}
mock_response.configure_mock(**attr)
mock_session_get.return_value = mock_response

with self.assertRaises(exceptions.SlowRetrievalError):
self.fetcher.download_bytes(self.url, self.file_length)

# Read/connect session timeout error
@patch.object(
urllib3.PoolManager,
Expand Down
7 changes: 7 additions & 0 deletions tuf/ngclient/urllib3_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,16 @@ def _chunks(

try:
yield from response.stream(self.chunk_size)
except urllib3.exceptions.TimeoutError as e:
# Raised directly when the gap timeout expires mid-stream:
# ReadTimeoutError is a TimeoutError but not a MaxRetryError.
raise exceptions.SlowRetrievalError from e
except urllib3.exceptions.MaxRetryError as e:
if isinstance(e.reason, urllib3.exceptions.TimeoutError):
raise exceptions.SlowRetrievalError from e
# Any other reason: propagate rather than ending the stream
# silently, which would look like a complete download.
raise

finally:
response.release_conn()