Skip to content

fix: retry Smartling API calls on 429/5xx to avoid batch-job failures - #657

Closed
markdaugherty wants to merge 3 commits into
adobe:mainfrom
markdaugherty:smartling-connector-rate-limit
Closed

fix: retry Smartling API calls on 429/5xx to avoid batch-job failures#657
markdaugherty wants to merge 3 commits into
adobe:mainfrom
markdaugherty:smartling-connector-rate-limit

Conversation

@markdaugherty

@markdaugherty markdaugherty commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary

The Smartling connector had no retry/backoff logic anywhere, so any 429 (rate limit) or transient 5xx from Smartling's API failed the call outright. Two spots make this especially likely during larger batch jobs:

  • getStatusAll polls /file/progress once per url in a tight sequential loop, no delay, no retry.
  • saveItems downloads translated files 5-at-a-time via a concurrency-5 Queue.

Per Smartling's own docs (via search — the page itself sits behind a Cloudflare challenge), they enforce two separate limits: a request-rate limit and a concurrent-request limit, scoped per user/project/account depending on endpoint. Recommended handling is exponential backoff with jitter, capped at 30–60s, 5–10 max retries.

Changes

  • New shared nx/blocks/loc/utils/fetchWithRetry.js: exponential backoff + jitter, honors Retry-After when present, configurable maxRetries/baseDelayMs/maxDelayMs/isRetryable (defaults: 5 retries, 500ms base, 30s cap, retries on 429 and 5xx).
  • Every fetch call in the Smartling connector now goes through it.
  • Built as a shared utility rather than a one-off, since Lionbridge's connector (fix: lionbridge connector correctness/compliance fixes + test parity #656) needed the identical fix — that PR has been updated to use this same utility instead of its own inline copy, rather than maintain two copies.
  • Bug fix found during live verification (nx/blocks/loc/views/translate/translate.js): setupService() built this._service via a spread/copy of this.project.options.service, taken once when the Translate view connects, before any job exists. Smartling's sendAllLanguages later sets jobUid directly on the original object; the copy never saw it, so getStatusAll crashed (Cannot read properties of undefined (reading 'value')) if you checked status in the same session without a page reload in between. Fixed to augment/reference the same object instead of copying it. Not Smartling-specific — affects any connector storing request state on service rather than per-lang.

Tests

  • New test/loc/utils/fetchWithRetry.test.js: retry-then-succeed on 429 (honoring Retry-After) and on 503 (exponential backoff), gives up after maxRetries, ignores non-retryable statuses, custom isRetryable override.
  • Two new regression tests in the Smartling suite confirming getStatusAll and saveItems actually retry a 429 and succeed on the next attempt.
  • Full npm test (1204 tests) and lint pass.

Verification

Full end-to-end through the real DA Translate app UI (da.live/apps/loc) against a real test site (scdemos/smartling-demo — new GitHub repo + AEM Code Sync + DA content) and a real Smartling project (project-scoped API token, per Smartling's own recommendation for dev/testing): created a project, sent a real page for translation, checked status, and confirmed real machine-translated French content landed at /fr/test-page.html (not just an echo — genuinely translated by Smartling).

Also confirmed along the way that Smartling's connector must go through the https://translate.da.live proxy (never api.smartling.com directly from the browser — that hits a CORS block), which resolveOrigin's legacy-rewrite logic already assumes but isn't otherwise documented anywhere.

Marking as draft pending final review pass.

Mark Daugherty added 3 commits August 11, 2026 15:10
The Smartling connector had no retry/backoff anywhere, so any 429 (rate
limit) or transient 5xx from Smartling's API failed the call outright.
Two spots make this likely during batch jobs: getStatusAll polls
/file/progress once per url in a tight sequential loop, and saveItems
downloads translated files 5-at-a-time via a concurrency-5 Queue —
Smartling enforces both a request-rate limit and a separate concurrent-
request limit, so either path can trip a 429.

Adds a shared nx/blocks/loc/utils/fetchWithRetry.js (exponential backoff
+ jitter, honoring Retry-After when present, configurable retry count/
delay/retryable-status predicate) and wires every fetch in the Smartling
connector through it. Intended to also replace Lionbridge's inline copy
of the same logic (nx/blocks/loc/connectors/lionbridge/index.js, in
adobe#656) once that lands, rather than maintain two copies.
Found while verifying the Smartling rate-limit fix end-to-end: getStatusAll
threw "Cannot read properties of undefined (reading 'value')" reading
service.jobUid.value, in the same session right after a successful send
(no reload in between).

setupService built this._service via a spread of
this.project.options.service, taken once when the Translate view first
connects, before any job exists. Smartling's sendAllLanguages later sets
jobUid directly on the original options.service object; since
this._service was a shallow copy, it never saw that mutation. A full
page reload masked this by rebuilding this._service fresh from the
now-persisted project, which is presumably why it went unnoticed.

Now augments and keeps a reference to the same service object instead
of copying it, matching how connectors already mutate it in place
(e.g. Lionbridge's service.jobId = { value: jobId }). Not
Smartling-specific — this affects every connector that stores request
state on the service object rather than per-lang.
@markdaugherty

Copy link
Copy Markdown
Member Author

Superseded by #662, which consolidates this with #661 into a single PR.

@markdaugherty
markdaugherty deleted the smartling-connector-rate-limit branch August 12, 2026 19:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant