Skip to content

Commit ae34cd7

Browse files
sjsyrekclaude
andcommitted
docs: describe the idempotency-aware retry policy and record the API-client fix batch
The README retry section now matches the shipped transport behavior: idempotent-only automatic retry, POST replay restricted to provably-unsent requests, 429 retried for all methods with Retry-After, full-jitter backoff instead of the fixed ladder the section invented, and the new --timeout / --max-retries global flags. CHANGELOG gains the Unreleased entries for the retry, classification, deadline, document-download, voice-reconnect, and watch-stats fixes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 105ef6b commit ae34cd7

2 files changed

Lines changed: 17 additions & 8 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- **ci**: Pushing a `v*` tag now runs the full release pipeline. The npm publish job is enabled (it was hard-disabled with `if: false` since its introduction, which is why the repo has 17 tags and zero GitHub Releases); it authenticates with a granular `NPM_TOKEN` for the first publish and keeps `--provenance` attestation. A new, deliberately independent `release` job creates a GitHub Release from the tag, with notes extracted from the version's CHANGELOG section (falling back to generated notes if the section is missing) — a publish failure cannot suppress the Release, and vice versa. A `homebrew` formula-bump job (version + sha256 PR against `DeepL/homebrew-tap`) ships gated with `if: false` until the tap repo and its cross-repo token exist.
13+
- **cli**: Global `--timeout <ms>` and `--max-retries <n>` options override the HTTP transport defaults (30000 ms, 3 retries) for a single invocation. Neither was previously configurable from the CLI.
1314

1415
### Changed
1516

@@ -76,6 +77,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7677
- **glossary**: `deepl glossary add-entry` / `update-entry` reject terms containing a tab, carriage return, or newline instead of shifting every following column of the uploaded dictionary, and glossary import picks the TSV or CSV dialect once per file, so a quoted CSV field containing a tab is no longer split into garbage columns.
7778
- **sync**: `deepl sync audit` no longer uses the lock file's source hash as a stand-in for a translation. Divergent translations were reported as consistent and identical ones as an inconsistency displaying a hex hash. Targets that cannot be read are now listed separately as missing — a new additive `missingTargets` field in the `sync audit --format json` output.
7879
- **sync**: TMS request URLs are built with the URL API: a trailing slash on `server:` no longer produces a doubled separator, a base path is preserved, and a URL with a query string or fragment is rejected instead of silently truncating the API path. TMS timeouts now exit with the network-error code (5) instead of 1, TMS error messages redact credentials embedded in the server URL, and `deepl sync pull` enforces a 32 MiB cap on the response body while reading it, instead of after the whole payload had been parsed.
80+
- **api**: Non-idempotent POST requests are no longer re-submitted after a client-side timeout. Every failure short of a 4xx reached the retry loop for all HTTP methods, so a batch or document upload that outlived the 30 s timeout was silently re-sent up to three more times — each already accepted and billed server-side (confirmed with server-side request counts: 4 per timed-out translate and upload), with the worst case being duplicate admin API keys whose secret is returned only once. Automatic retry is now restricted to idempotent methods (GET, HEAD, PUT, DELETE); a POST is replayed only on an error that proves the request never reached the server (`ECONNREFUSED`, `ENOTFOUND`, `EAI_AGAIN`). A 429 is still retried for every method, honoring `Retry-After`.
81+
- **api**: A client-side timeout now exits 5 (network error) instead of 6 (invalid input), matching the documented exit-code contract. Error classification substring-matched the message and missed axios's `timeout of 30000ms exceeded` (`ECONNABORTED` and `ERR_CANCELED` were absent entirely), falling through to `ValidationError` — so CI that retries on 5 and hard-fails on 6 did exactly the wrong thing on a flaky network. Classification now branches on `error.code` and the absence of a response. An HTTP 401 is likewise mapped to `AuthError` (exit 2) instead of falling through to exit 6.
82+
- **api**: Retries run under an overall time budget rather than only a per-attempt timeout, so a never-responding server no longer holds a single command for two minutes (measured 125 s before). Honest `Retry-After` waits are not charged against the budget.
83+
- **api**: The document translation result endpoint is never retried — the download is effectively single-use, so a retry after a timeout on a large file could permanently lose an already-billed translation — and document transfers get their own larger timeout.
84+
- **voice**: `deepl voice` now actually reconnects after a transport failure. The socket `error` handler marked the stream ended before the `close` event that always follows arrived, so the reconnect path (up to 3 attempts, `--reconnect` on by default) was unreachable for a real network drop — only a clean remote close ever reconnected. A reconnect that exhausts its attempts now closes the audio input instead of leaving the stream generator awaiting forever.
85+
- **watch**: `deepl watch` statistics now report the number of watched files; the counter was never incremented.
7986

8087
### Security
8188

‎README.md‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,22 +1047,24 @@ DeepL CLI includes built-in retry logic and timeout handling for robust API comm
10471047

10481048
**Automatic Retry Logic:**
10491049

1050-
- Automatically retries failed requests on transient errors (5xx, network failures)
1051-
- Default: 3 retries with exponential backoff
1052-
- Does not retry on client errors (4xx - bad request, auth failures, etc.)
1053-
- Exponential backoff delays: 1s, 2s, 4s, 8s, 10s (capped at 10s)
1050+
- Idempotent requests (GET, HEAD, PUT, DELETE) are retried on 5xx responses and transient network failures
1051+
- Non-idempotent requests (POST — translations, uploads, glossary and key creation) are replayed only when the error proves the request never reached the server (connection refused, DNS failure), so a slow response can never be submitted and billed twice
1052+
- Rate limiting (429) is retried for all methods, honoring the `Retry-After` header when the server sends one
1053+
- Other client errors (4xx — bad request, auth failures, etc.) are not retried
1054+
- Default: 3 retries with full-jitter exponential backoff (capped at 10s)
10541055

10551056
**Timeout Configuration:**
10561057

1057-
- Default timeout: 30 seconds per request
1058+
- Default timeout: 30 seconds per request, with an overall time budget across retries
10581059
- Applies to all API requests (translate, usage, languages, etc.)
1060+
- Override per invocation with the global `--timeout <ms>` and `--max-retries <n>` flags
10591061

10601062
**Features:**
10611063

10621064
- ✅ Automatic retry on transient failures
10631065
- ✅ Exponential backoff to avoid overwhelming the API
1064-
- ✅ Smart error detection (retries 5xx, not 4xx)
1065-
- ✅ Configurable via library-consumer options; not exposed as a CLI flag
1066+
- ✅ Retry policy aware of request idempotency
1067+
- ✅ Configurable via `--timeout` / `--max-retries` (or library-consumer options)
10661068
- ✅ Works across all DeepL API endpoints
10671069

10681070
**Retry Behavior Examples:**
@@ -1081,7 +1083,7 @@ deepl translate "Hello" --to es
10811083
# otherwise falls back to jittered exponential backoff
10821084
```
10831085

1084-
**Note:** Retry and timeout settings use sensible defaults optimized for the DeepL API. These are internal features that work automatically - no configuration required.
1086+
**Note:** Retry and timeout settings use sensible defaults optimized for the DeepL API. No configuration is required; `--timeout` and `--max-retries` are available when a specific invocation needs different bounds (e.g. very large documents on a slow uplink).
10851087

10861088
### Glossaries
10871089

0 commit comments

Comments
 (0)