@@ -137,6 +137,108 @@ def test_session_get_timeout(self, mock_session_get: Mock) -> None:
137137 self .fetcher .fetch (self .url )
138138 mock_session_get .assert_called_once ()
139139
140+ # Test retry on ReadTimeoutError during streaming
141+ @patch .object (urllib3 .PoolManager , "request" )
142+ def test_download_bytes_retry_on_streaming_timeout (
143+ self , mock_request : Mock
144+ ) -> None :
145+ """Test that download_bytes retries when ReadTimeoutError occurs during streaming."""
146+ mock_response_fail = Mock ()
147+ mock_response_fail .status = 200
148+ mock_response_fail .stream .side_effect = (
149+ urllib3 .exceptions .ReadTimeoutError (
150+ urllib3 .connectionpool .ConnectionPool ("localhost" ),
151+ "" ,
152+ "Read timed out" ,
153+ )
154+ )
155+
156+ mock_response_success = Mock ()
157+ mock_response_success .status = 200
158+ mock_response_success .stream .return_value = iter (
159+ [self .file_contents [:4 ], self .file_contents [4 :]]
160+ )
161+
162+ mock_request .side_effect = [
163+ mock_response_fail ,
164+ mock_response_fail ,
165+ mock_response_success ,
166+ ]
167+
168+ data = self .fetcher .download_bytes (self .url , self .file_length )
169+ self .assertEqual (self .file_contents , data )
170+ self .assertEqual (mock_request .call_count , 3 )
171+
172+ # Test retry exhaustion
173+ @patch .object (urllib3 .PoolManager , "request" )
174+ def test_download_bytes_retry_exhaustion (self , mock_request : Mock ) -> None :
175+ """Test that download_bytes fails after exhausting all retries."""
176+ # All attempts fail
177+ mock_response = Mock ()
178+ mock_response .status = 200
179+ mock_response .stream .side_effect = urllib3 .exceptions .ReadTimeoutError (
180+ urllib3 .connectionpool .ConnectionPool ("localhost" ),
181+ "" ,
182+ "Read timed out" ,
183+ )
184+ mock_request .return_value = mock_response
185+
186+ with self .assertRaises (exceptions .SlowRetrievalError ):
187+ self .fetcher .download_bytes (self .url , self .file_length )
188+ # Should have been called 3 times (max_retries=3)
189+ self .assertEqual (mock_request .call_count , 3 )
190+
191+ # Test retry on ProtocolError during streaming
192+ @patch .object (urllib3 .PoolManager , "request" )
193+ def test_download_bytes_retry_on_protocol_error (
194+ self , mock_request : Mock
195+ ) -> None :
196+ """Test that download_bytes retries when ProtocolError occurs during streaming."""
197+ # First attempt fails with protocol error, second succeeds
198+ mock_response_fail = Mock ()
199+ mock_response_fail .status = 200
200+ mock_response_fail .stream .side_effect = (
201+ urllib3 .exceptions .ProtocolError ("Connection broken" )
202+ )
203+
204+ mock_response_success = Mock ()
205+ mock_response_success .status = 200
206+ mock_response_success .stream .return_value = iter (
207+ [self .file_contents [:4 ], self .file_contents [4 :]]
208+ )
209+
210+ mock_request .side_effect = [
211+ mock_response_fail ,
212+ mock_response_success ,
213+ ]
214+
215+ data = self .fetcher .download_bytes (self .url , self .file_length )
216+ self .assertEqual (self .file_contents , data )
217+ self .assertEqual (mock_request .call_count , 2 )
218+
219+ # Test that non-timeout errors are not retried
220+ @patch .object (urllib3 .PoolManager , "request" )
221+ def test_download_bytes_no_retry_on_http_error (
222+ self , mock_request : Mock
223+ ) -> None :
224+ """Test that download_bytes does not retry on HTTP errors like 404."""
225+ mock_response = Mock ()
226+ mock_response .status = 404
227+ mock_request .return_value = mock_response
228+
229+ with self .assertRaises (exceptions .DownloadHTTPError ):
230+ self .fetcher .download_bytes (self .url , self .file_length )
231+ # Should only be called once, no retries
232+ mock_request .assert_called_once ()
233+
234+ # Test that length mismatch errors are not retried
235+ def test_download_bytes_no_retry_on_length_mismatch (self ) -> None :
236+ """Test that download_bytes does not retry on length mismatch errors."""
237+ # Try to download more data than the file contains
238+ with self .assertRaises (exceptions .DownloadLengthMismatchError ):
239+ # File is self.file_length bytes, asking for less should fail
240+ self .fetcher .download_bytes (self .url , self .file_length - 4 )
241+
140242 # Simple bytes download
141243 def test_download_bytes (self ) -> None :
142244 data = self .fetcher .download_bytes (self .url , self .file_length )
0 commit comments