Skip to content

fix(utils): honor retry-after for translation retries#2279

Open
g9420 wants to merge 1 commit into
refly-ai:mainfrom
g9420:codex/fix-2278-retry-after
Open

fix(utils): honor retry-after for translation retries#2279
g9420 wants to merge 1 commit into
refly-ai:mainfrom
g9420:codex/fix-2278-retry-after

Conversation

@g9420

@g9420 g9420 commented Jun 9, 2026

Copy link
Copy Markdown

Fixes #2278

Summary

  • Honor Retry-After for 429 translation retry responses
  • Support both delta-seconds and HTTP-date Retry-After values
  • Keep exponential backoff fallback when Retry-After is missing or invalid
  • Add regression coverage for 429 retry timing

Tests

  • pnpm --filter @refly/utils exec vitest run src/translate.test.ts
  • pnpm exec biome check packages/utils/src/translate/index.ts packages/utils/src/translate.test.ts

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced translation service retry behavior to respect HTTP Retry-After headers from rate-limit and server error responses, implementing intelligent retry delays. Falls back to exponential backoff when headers are unavailable, improving reliability during high-load scenarios.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds support for respecting HTTP Retry-After headers in the translation retry logic. A new parseRetryAfterDelay helper converts header values to millisecond delays. The calculateRetryDelay function now uses this delay when available, falling back to exponential backoff otherwise. The retry path passes the Retry-After header to this calculation. A test validates that retries wait the specified delay before attempting again.

Changes

Retry-After header support

Layer / File(s) Summary
Retry-After header parsing and delay calculation
packages/utils/src/translate/index.ts
Added parseRetryAfterDelay to parse Retry-After values as seconds or HTTP date into millisecond delays. Updated calculateRetryDelay to accept optional retryAfter parameter and use the parsed delay when available, otherwise use exponential backoff.
FetchWithRetry integration and behavior test
packages/utils/src/translate/index.ts, packages/utils/src/translate.test.ts
Wired Retry-After header into the 429 retry path by passing response.headers.get('Retry-After') to calculateRetryDelay. Added test that stubs fetch to return 429 with Retry-After header, uses fake timers to verify no immediate retry, advances past the delay, and confirms eventual success.

🎯 2 (Simple) | ⏱️ ~12 minutes

🐰 A header said "wait sixty" with patient pride,
But retries once rushed, exhausted and died,
Now logic respects what the server proclaims,
The retry dance waits, and wins the good names! 🎭

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: implementing Retry-After header support for translation retry logic.
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #2278: respects Retry-After header on 429 responses, supports both delta-seconds and HTTP-date formats, preserves exponential backoff as fallback, and includes regression test coverage.
Out of Scope Changes check ✅ Passed All changes are strictly scoped to the linked issue: test file for 429 retry behavior and modified retry-delay logic to honor Retry-After header.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/utils/src/translate/index.ts (1)

122-125: ⚡ Quick win

Consider supporting Retry-After for 503 responses.

The current implementation only honors Retry-After for 429 rate-limit responses. The HTTP specification also allows Retry-After with 503 Service Unavailable responses, which is commonly used during maintenance windows or temporary outages.

♻️ Proposed enhancement to support 503
 const retryDelay = calculateRetryDelay(
   attempt,
-  response.status === 429 ? response.headers.get('Retry-After') : null,
+  response.status === 429 || response.status === 503
+    ? response.headers.get('Retry-After')
+    : null,
 );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utils/src/translate/index.ts` around lines 122 - 125, The retry
delay calculation currently only passes the 'Retry-After' header when
response.status === 429; update the conditional in the call to
calculateRetryDelay so it also passes the header when response.status === 503
(i.e., check for 429 || 503 or otherwise include 503). Locate the call to
calculateRetryDelay in translate/index.ts and ensure the second argument uses
response.headers.get('Retry-After') for both 429 and 503 responses so
maintenance/outage 503 responses honor Retry-After.
packages/utils/src/translate.test.ts (1)

17-45: Test correctly validates retry timing; consider adding HTTP-date format coverage.

The test properly verifies that a 429 response with Retry-After: 60 waits the full 60 seconds before retrying. The use of fake timers and the check at 1 second confirms no early retry occurs.

Consider adding a second test case to verify the HTTP-date format for Retry-After, since parseRetryAfterDelay supports both formats:

📋 Example test for HTTP-date format
it('should wait for Retry-After HTTP-date before retrying a 429 response', async () => {
  const futureDate = new Date(Date.now() + 30000).toUTCString();
  const fetchMock = vi
    .fn()
    .mockResolvedValueOnce({
      ok: false,
      status: 429,
      headers: new Headers({ 'Retry-After': futureDate }),
    })
    .mockResolvedValueOnce({
      ok: true,
      status: 200,
      json: async () => [[['hola']]],
    });

  vi.stubGlobal('fetch', fetchMock);

  const result = translateText('hello', 'es');
  await Promise.resolve();

  expect(fetchMock).toHaveBeenCalledTimes(1);

  await vi.advanceTimersByTimeAsync(29000);
  expect(fetchMock).toHaveBeenCalledTimes(1);

  await vi.advanceTimersByTimeAsync(1000);

  await expect(result).resolves.toBe('hola');
  expect(fetchMock).toHaveBeenCalledTimes(2);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utils/src/translate.test.ts` around lines 17 - 45, Add a second unit
test that mirrors the existing 429/Retry-After behavior but supplies an
HTTP-date string in the Retry-After header to exercise parseRetryAfterDelay's
date parsing; in the test create a futureDate via new Date(Date.now() +
30000).toUTCString(), mock the first fetch to return status 429 with that header
and the second to return ok 200 with the expected JSON, stubGlobal('fetch'),
call translateText('hello', 'es'), advance fake timers until just before the
date then past it to assert no early retry and that the final result resolves to
'hola' and fetch was called twice (referencing parseRetryAfterDelay and
translateText to locate the logic to cover).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/utils/src/translate/index.ts`:
- Around line 17-43: The parseRetryAfterDelay function currently returns parsed
Retry-After delays unbounded, allowing values larger than the configured cap;
update parseRetryAfterDelay (used by calculateRetryDelay) to clamp returned
delays to RETRY_CONFIG.maxDelay by returning Math.min(parsedDelay,
RETRY_CONFIG.maxDelay) for both numeric-second and HTTP-date branches so
Retry-After values cannot exceed the global maxDelay.

---

Nitpick comments:
In `@packages/utils/src/translate.test.ts`:
- Around line 17-45: Add a second unit test that mirrors the existing
429/Retry-After behavior but supplies an HTTP-date string in the Retry-After
header to exercise parseRetryAfterDelay's date parsing; in the test create a
futureDate via new Date(Date.now() + 30000).toUTCString(), mock the first fetch
to return status 429 with that header and the second to return ok 200 with the
expected JSON, stubGlobal('fetch'), call translateText('hello', 'es'), advance
fake timers until just before the date then past it to assert no early retry and
that the final result resolves to 'hola' and fetch was called twice (referencing
parseRetryAfterDelay and translateText to locate the logic to cover).

In `@packages/utils/src/translate/index.ts`:
- Around line 122-125: The retry delay calculation currently only passes the
'Retry-After' header when response.status === 429; update the conditional in the
call to calculateRetryDelay so it also passes the header when response.status
=== 503 (i.e., check for 429 || 503 or otherwise include 503). Locate the call
to calculateRetryDelay in translate/index.ts and ensure the second argument uses
response.headers.get('Retry-After') for both 429 and 503 responses so
maintenance/outage 503 responses honor Retry-After.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3d2add22-96a1-41ed-90ee-da7ef2b6486b

📥 Commits

Reviewing files that changed from the base of the PR and between 330c01b and d98480f.

📒 Files selected for processing (2)
  • packages/utils/src/translate.test.ts
  • packages/utils/src/translate/index.ts

Comment on lines 17 to +43
/**
* Calculate retry delay time with exponential backoff
* Parse Retry-After header value.
* @param retryAfter Retry-After header value
* @returns Delay time in milliseconds, or null if the header is invalid
*/
const parseRetryAfterDelay = (retryAfter: string | null): number | null => {
if (!retryAfter) {
return null;
}

const retryAfterValue = retryAfter.trim();
if (!retryAfterValue) {
return null;
}

const retryAfterSeconds = Number(retryAfterValue);
if (Number.isFinite(retryAfterSeconds)) {
return retryAfterSeconds >= 0 ? retryAfterSeconds * 1000 : null;
}

const retryAfterDate = Date.parse(retryAfterValue);
if (!Number.isNaN(retryAfterDate)) {
return Math.max(retryAfterDate - Date.now(), 0);
}

return null;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for timeout configurations or retry budget limits that might bound this behavior

# Search for timeout configurations in the codebase
rg -n -C3 'timeout|TimeoutError|AbortSignal' --type=ts --glob '!node_modules'

# Search for any retry budget or circuit breaker patterns
rg -n -C3 'retry.*budget|circuit.*breaker|max.*wait|timeout.*config' --type=ts --glob '!node_modules'

Repository: refly-ai/refly

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "parseRetryAfterDelay|RETRY_CONFIG|maxDelay|Retry-After" packages/utils/src/translate/index.ts
echo "---- context around RETRY_CONFIG ----"
rg -n "RETRY_CONFIG" packages/utils/src/translate/index.ts -n
echo "---- printing translate index.ts head (1-200) ----"
sed -n '1,220p' packages/utils/src/translate/index.ts

Repository: refly-ai/refly

Length of output: 8596


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "parseRetryAfterDelay\(" packages/utils/src/translate/index.ts
rg -n "calculateRetryDelay\(" packages/utils/src/translate/index.ts
rg -n "maxDelay" packages/utils/src/translate/index.ts

Repository: refly-ai/refly

Length of output: 348


Cap parsed Retry-After delay to RETRY_CONFIG.maxDelay

calculateRetryDelay returns the parsed Retry-After value as-is when valid, bypassing the Math.min(..., RETRY_CONFIG.maxDelay) cap that’s applied only to the exponential-backoff path—so Retry-After: 3600 (or a far-future HTTP date) can produce waits far longer than the configured 10s. Consider return Math.min(retryAfterDelay, RETRY_CONFIG.maxDelay) (or document intentional unbounded behavior).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utils/src/translate/index.ts` around lines 17 - 43, The
parseRetryAfterDelay function currently returns parsed Retry-After delays
unbounded, allowing values larger than the configured cap; update
parseRetryAfterDelay (used by calculateRetryDelay) to clamp returned delays to
RETRY_CONFIG.maxDelay by returning Math.min(parsedDelay, RETRY_CONFIG.maxDelay)
for both numeric-second and HTTP-date branches so Retry-After values cannot
exceed the global maxDelay.

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.

translate/index.ts — 429 retries at 1s, 2s, 4s ignore Retry-After, retry budget exhausted in 7 seconds

2 participants