Skip to content

Commit 455d8ac

Browse files
hive-botclaude
andcommitted
fix: report retryable storage URL errors with their response body
Errors from the pre-signed translation memory upload and download URLs now set should_retry for HTTP 429 and 5xx, and a failed streamed download includes its response body in the message. They stay a plain DeepLException with http_status_code, as these URLs are not DeepL API endpoints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent afd474d commit 455d8ac

4 files changed

Lines changed: 51 additions & 5 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
242242
connect to the proxy), are no longer retried and raise `ConnectionException`
243243
with `should_retry` `False`.
244244
- Query string values encode a space as `%20` instead of `+`.
245+
- Fixed errors from the pre-signed translation memory upload and download URLs
246+
reporting `should_retry` as false for HTTP 429 and 5xx, and dropping the
247+
response body of a failed download from the message.
245248
- HTTP 401 responses now raise `AuthorizationException`, like 403. Before they
246249
raised `DeepLException` with "Unexpected status code: 401".
247250
- `DeepLException.should_retry` is now `True` for every HTTP 5xx error. It was

‎README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,6 +1268,13 @@ job = deepl_client.wait_until_translation_memory_job_done(
12681268
)
12691269
```
12701270

1271+
Errors from the pre-signed upload and download URLs are not DeepL API errors:
1272+
`upload_translation_memory_file()` and `download_translation_memory_export()`
1273+
raise a plain `DeepLException` with the response body in the message,
1274+
`http_status_code` set, and `should_retry` true for HTTP 429 and 5xx. An
1275+
expired URL (for example HTTP 403) therefore does not raise
1276+
`AuthorizationException`.
1277+
12711278
`TranslationMemoryJob.status` returns a `TranslationMemoryJobStatus` member
12721279
(`AWAITING_INPUT`, `PROCESSING`, `COMPLETED`, `FAILED` or `EXPIRED`), or the
12731280
plain string for a status the library does not know. Members are strings and

‎deepl/deepl_client.py‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2061,6 +2061,9 @@ def upload_translation_memory_file(
20612061
.. deprecated::
20622062
Use content instead. input_file will be removed in the next
20632063
major version.
2064+
2065+
:raises DeepLException: If the storage URL returns an error status,
2066+
with http_status_code set; should_retry is True for 429 and 5xx.
20642067
"""
20652068
if input_file is not None:
20662069
if content is not None:
@@ -2206,6 +2209,9 @@ def download_translation_memory_export(
22062209
default 64 KiB. Only used if output_file is specified.
22072210
:return: None if output_file is specified, otherwise the
22082211
requests.Response.
2212+
2213+
:raises DeepLException: If the storage URL returns an error status,
2214+
with http_status_code set; should_retry is True for 429 and 5xx.
22092215
"""
22102216
download_url = job.result.download_url if job.result else None
22112217
if not download_url:
@@ -2361,10 +2367,21 @@ def _raise_for_asset_status(
23612367
) -> None:
23622368
if 200 <= status_code < 300:
23632369
return
2364-
detail = content if isinstance(content, str) else ""
2370+
if isinstance(content, requests.Response):
2371+
# A streamed download: read the error body for the message.
2372+
try:
2373+
detail = content.text
2374+
except requests.exceptions.RequestException:
2375+
detail = ""
2376+
else:
2377+
detail = content
23652378
message = f"Error {action}, HTTP status: {status_code}"
23662379
if detail:
23672380
message += f", detail: {detail}"
2381+
# Storage URLs are not DeepL API endpoints, so every error is a plain
2382+
# DeepLException; 429 and 5xx are retryable as for the API.
23682383
raise DeepLException(
2369-
message, should_retry=False, http_status_code=status_code
2384+
message,
2385+
should_retry=status_code == 429 or status_code >= 500,
2386+
http_status_code=status_code,
23702387
)

‎tests/offline/test_translation_memories.py‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,24 @@ def test_raises_with_status_and_detail_when_storage_rejects_upload(mocked):
369369
assert exc_info.value.http_status_code == 403
370370

371371

372+
@pytest.mark.parametrize(
373+
"status, should_retry",
374+
[(429, True), (500, True), (503, True), (403, False)],
375+
)
376+
def test_marks_storage_errors_retryable_for_429_and_5xx(
377+
mocked, no_retries, status, should_retry
378+
):
379+
mocked.put(UPLOAD_URL, status=status)
380+
381+
with pytest.raises(deepl.DeepLException) as exc_info:
382+
make_deepl_client().upload_translation_memory_file(UPLOAD_URL, b"x")
383+
384+
# A plain DeepLException: storage URLs are not DeepL API endpoints.
385+
assert type(exc_info.value) is deepl.DeepLException
386+
assert exc_info.value.http_status_code == status
387+
assert exc_info.value.should_retry is should_retry
388+
389+
372390
@pytest.mark.parametrize("status, reused", [(200, True), (202, False)])
373391
def test_marks_export_as_reused_when_server_returns_200(
374392
mocked, status, reused
@@ -681,15 +699,16 @@ def test_raises_when_downloading_export_without_download_url():
681699
make_deepl_client().download_translation_memory_export(job)
682700

683701

684-
def test_raises_when_storage_rejects_download(mocked):
685-
mocked.get(DOWNLOAD_URL, status=404)
702+
def test_raises_with_body_when_storage_rejects_streamed_download(mocked):
703+
mocked.get(DOWNLOAD_URL, status=404, body="no such key")
686704
job = TranslationMemoryJob.from_json(
687705
job_json("completed", "export", download_url=DOWNLOAD_URL)
688706
)
689707

690708
with pytest.raises(
691709
deepl.DeepLException,
692-
match="downloading translation memory export, HTTP status: 404",
710+
match="downloading translation memory export, HTTP status: 404, "
711+
"detail: no such key",
693712
):
694713
make_deepl_client().download_translation_memory_export(
695714
job, io.BytesIO()

0 commit comments

Comments
 (0)