@@ -159,16 +159,29 @@ def do(self,
159159 if isinstance (data , (str , bytes )):
160160 data = io .BytesIO (data .encode ('utf-8' ) if isinstance (data , str ) else data )
161161
162- # Only retry if the request is not a stream or if the stream is seekable and
163- # we can rewind it. This is necessary to avoid bugs where the retry doesn't
164- # re-read already read data from the body.
165- if data is not None and not self ._is_seekable_stream (data ):
166- logger .debug (f"Retry disabled for non-seekable stream: type={ type (data )} " )
167- call = self ._perform
168- else :
162+ if not data :
163+ # The request is not a stream.
169164 call = retried (timeout = timedelta (seconds = self ._retry_timeout_seconds ),
170165 is_retryable = self ._is_retryable ,
171166 clock = self ._clock )(self ._perform )
167+ elif self ._is_seekable_stream (data ):
168+ # Keep track of the initial position of the stream so that we can rewind to it
169+ # if we need to retry the request.
170+ initial_data_position = data .tell ()
171+
172+ def rewind ():
173+ logger .debug (f"Rewinding input data to offset { initial_data_position } before retry" )
174+ data .seek (initial_data_position )
175+
176+ call = retried (timeout = timedelta (seconds = self ._retry_timeout_seconds ),
177+ is_retryable = self ._is_retryable ,
178+ clock = self ._clock ,
179+ before_retry = rewind )(self ._perform )
180+ else :
181+ # Do not retry if the stream is not seekable. This is necessary to avoid bugs
182+ # where the retry doesn't re-read already read data from the stream.
183+ logger .debug (f"Retry disabled for non-seekable stream: type={ type (data )} " )
184+ call = self ._perform
172185
173186 response = call (method ,
174187 url ,
@@ -249,12 +262,6 @@ def _perform(self,
249262 files = None ,
250263 data = None ,
251264 auth : Callable [[requests .PreparedRequest ], requests .PreparedRequest ] = None ):
252- # Keep track of the initial position of the stream so that we can rewind it if
253- # we need to retry the request.
254- initial_data_position = 0
255- if self ._is_seekable_stream (data ):
256- initial_data_position = data .tell ()
257-
258265 response = self ._session .request (method ,
259266 url ,
260267 params = self ._fix_query_string (query ),
@@ -266,16 +273,8 @@ def _perform(self,
266273 stream = raw ,
267274 timeout = self ._http_timeout_seconds )
268275 self ._record_request_log (response , raw = raw or data is not None or files is not None )
269-
270276 error = self ._error_parser .get_api_error (response )
271277 if error is not None :
272- # If the request body is a seekable stream, rewind it so that it is ready
273- # to be read again in case of a retry.
274- #
275- # TODO: This should be moved into a "before-retry" hook to avoid one
276- # unnecessary seek on the last failed retry before aborting.
277- if self ._is_seekable_stream (data ):
278- data .seek (initial_data_position )
279278 raise error from None
280279
281280 return response
0 commit comments