Skip to content

Commit f17469c

Browse files
committed
Multipart upload
1 parent 3a32788 commit f17469c

3 files changed

Lines changed: 48 additions & 1 deletion

File tree

‎atlassian/VERSION‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5.0.0
1+
5.0.1

‎atlassian/rest_client.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,8 +479,23 @@ def request(
479479

480480
headers = headers or self.default_headers
481481

482+
# ``requests`` reads file-like multipart values when preparing a
483+
# request. Reset them before every attempt so a retry cannot upload an
484+
# already-consumed, zero-byte file.
485+
file_positions = []
486+
if files:
487+
for upload in files.values():
488+
stream = upload[1] if isinstance(upload, (tuple, list)) and len(upload) > 1 else upload
489+
if hasattr(stream, "seek") and hasattr(stream, "tell"):
490+
try:
491+
file_positions.append((stream, stream.tell()))
492+
except (OSError, ValueError):
493+
pass
494+
482495
retry_handler = self._retry_handler()
483496
while True:
497+
for stream, position in file_positions:
498+
stream.seek(position)
484499
self.log_curl_debug(
485500
method=method,
486501
url=url,

‎tests/test_rest_client.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Unit tests for atlassian.rest_client module
44
"""
55

6+
import io
67
from datetime import datetime, timedelta
78
from types import SimpleNamespace
89

@@ -184,6 +185,37 @@ def test_advanced_mode(self):
184185
api = AtlassianRestAPI(url=f"{mockup_server()}/test", advanced_mode=True)
185186
assert api.advanced_mode is True
186187

188+
def test_request_rewinds_uploaded_file_before_retry(self):
189+
class RetryingSession:
190+
def __init__(self):
191+
self.verify = True
192+
self.payloads = []
193+
self.responses = [
194+
SimpleNamespace(status_code=503, headers={}, reason="Unavailable", text=""),
195+
SimpleNamespace(status_code=200, headers={}, reason="OK", text=""),
196+
]
197+
198+
def request(self, **kwargs):
199+
self.payloads.append(kwargs["files"]["file"][1].read())
200+
return self.responses.pop(0)
201+
202+
session = RetryingSession()
203+
api = AtlassianRestAPI(
204+
url="https://example.test",
205+
session=session,
206+
advanced_mode=True,
207+
retry_status_codes=[503],
208+
max_backoff_retries=1,
209+
backoff_factor=0,
210+
backoff_jitter=0,
211+
)
212+
api.backoff_and_retry = True
213+
api.use_urllib3_retry = False
214+
215+
api.request("POST", "attachment", files={"file": ("report.csv", io.BytesIO(b"small report"))})
216+
217+
assert session.payloads == [b"small report", b"small report"]
218+
187219
def test_kerberos_configuration(self):
188220
"""Test kerberos configuration"""
189221
# Test that kerberos config is accepted without errors

0 commit comments

Comments
 (0)