Skip to content

Commit 477a196

Browse files
hive-botclaude
andcommitted
fix: map storage-URL errors of the export download as released
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d5c765a commit 477a196

4 files changed

Lines changed: 22 additions & 34 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -325,11 +325,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
325325
`exportTranslationMemoryToFilepath()` keep polling on an unknown status or a
326326
job without results instead of rejecting; the timeout bounds the wait.
327327
- Translation memory and job IDs are URL-encoded in request paths.
328-
- `downloadTranslationMemoryExport()` and `exportTranslationMemoryToFilepath()`
329-
throw a plain `DeepLError` with `httpStatusCode` (and `shouldRetry` true for
330-
429 and 5xx) when the pre-signed download URL answers an error. Before, the
331-
status went through the DeepL API mapping, so for example an expired URL (403)
332-
threw `AuthorizationError` and a 503 threw `DocumentNotReadyError`.
333328
- `exportTranslationMemoryToFilepath()` checks for an existing output file and
334329
an unwritable or missing output directory before it creates the export job,
335330
and throws the same Node error (`EEXIST`, `ENOENT`, `EACCES`) as before;

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1500,7 +1500,9 @@ connections, unreachable hosts or networks and connect timeouts), as resending i
15001500
requests throw its deprecated subclass `GlossaryNotFoundError`.
15011501
- `TooManyRequestsError`: HTTP 429, after all retries.
15021502
- `QuotaExceededError`: HTTP 456, the quota of the billing period is used up.
1503-
- `DocumentNotReadyError`: HTTP 503 while downloading a translated document.
1503+
- `DocumentNotReadyError`: HTTP 503 while getting the status of a document
1504+
translation, downloading a translated document or downloading a
1505+
translation memory export.
15041506
- `ConnectionError`: the request got no HTTP response, for example a timeout.
15051507
- `DocumentTranslationError`: a document translation failed. The cause is in
15061508
its `error` property, and `httpStatusCode` and `shouldRetry` are copied

‎src/deeplClient.ts‎

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ function spokenTermsEntriesToJson(entries: SpokenTermsEntries): Record<string, u
129129
}
130130

131131
/**
132-
* Throws a plain DeepLError for a failed response from a pre-signed storage URL. These are not
132+
* Throws a plain DeepLError for a failed upload to a pre-signed storage URL. These are not
133133
* DeepL API responses, so the API's status code mapping (e.g. 403 to AuthorizationError) does
134134
* not apply.
135135
*/
@@ -1539,8 +1539,8 @@ export class DeepLClient extends Translator {
15391539
* @param outputFile: String containing output file path, or a WriteStream to store file data.
15401540
*
15411541
* @throws {DeepLError} If the job has no download URL, or any error occurs while downloading.
1542-
* An error response from the download URL is a plain DeepLError carrying httpStatusCode,
1543-
* never one of its subclasses.
1542+
* An error response from the download URL is mapped like a document download response
1543+
* (e.g. 403 to AuthorizationError, 503 to DocumentNotReadyError), with httpStatusCode set.
15441544
*/
15451545
async downloadTranslationMemoryExport(
15461546
job: Omit<TranslationMemoryJob, 'done' | 'ok' | 'creationTime' | 'updatedTime'>,
@@ -1574,13 +1574,9 @@ export class DeepLClient extends Translator {
15741574
{},
15751575
true,
15761576
);
1577-
if (statusCode < 200 || statusCode >= 300) {
1578-
await throwStorageError(
1579-
'Error downloading translation memory export',
1580-
statusCode,
1581-
content,
1582-
);
1583-
}
1577+
// The released status code mapping (e.g. 403 to AuthorizationError, 503 to
1578+
// DocumentNotReadyError) is kept until the next major version.
1579+
await checkStatusCode(statusCode, content, false, true);
15841580

15851581
if (fd === undefined) {
15861582
return await pipeToStream(content, outputFile as fs.WriteStream);

‎tests/offline/translationMemories.test.ts‎

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -556,24 +556,19 @@ describe('DeepLClient.downloadTranslationMemoryExport', () => {
556556
});
557557

558558
it.each([
559-
[403, false],
560-
[429, true],
561-
[503, true],
562-
])(
563-
'throws a plain DeepLError when the storage answers %i, with shouldRetry %s',
564-
async (status, shouldRetry) => {
565-
nock(storageUrl).get('/download').reply(status, 'storage error');
566-
567-
const error = await makeDeeplClient({ maxRetries: 0 })
568-
.downloadTranslationMemoryExport(job(`${storageUrl}/download`), 'unused.tmx')
569-
.catch((e) => e);
570-
571-
// Not AuthorizationError (403) or DocumentNotReadyError (503): storage is not the API
572-
expect(error.constructor).toBe(deepl.DeepLError);
573-
expect(error.message).toContain('storage error');
574-
expect(error).toMatchObject({ httpStatusCode: status, shouldRetry });
575-
},
576-
);
559+
[403, deepl.AuthorizationError, false],
560+
[503, deepl.DocumentNotReadyError, true],
561+
])('maps a %i from the storage as released, to %p', async (status, errorClass, shouldRetry) => {
562+
nock(storageUrl).get('/download').reply(status, 'storage error');
563+
564+
const error = await makeDeeplClient({ maxRetries: 0 })
565+
.downloadTranslationMemoryExport(job(`${storageUrl}/download`), 'unused.tmx')
566+
.catch((e) => e);
567+
568+
expect(error).toBeInstanceOf(errorClass);
569+
expect(error.message).toContain('storage error');
570+
expect(error).toMatchObject({ httpStatusCode: status, shouldRetry });
571+
});
577572

578573
it('throws ArgumentError when the job has no download URL', async () => {
579574
await expect(

0 commit comments

Comments
 (0)