Skip to content

feat(fetch): retry uncharged busy answers so a brief capacity wait does not fail the command - #64

Draft
gonzalo-zenrows wants to merge 1 commit into
mainfrom
feat/fetch-retry-jitter
Draft

gonzalo-zenrows wants to merge 1 commit into
mainfrom
feat/fetch-retry-jitter

Conversation

@gonzalo-zenrows

Copy link
Copy Markdown

Why

zenrows fetch and zenrows extract sent each request once. When the gateway was briefly busy (429, 502, 503, 504, none of which is charged) or the network dropped, the command failed. A 503 RESP009 ("at capacity, retry shortly") fell into the generic branch that told agents to rerun with --manual --js-render --premium-proxy, the most expensive configuration, which does not help.

This applies the retry policy the Zenrows Fetch/Extract clients share.

What changes

  • Retries. 429, 502, 503, 504 and network errors before any response are retried up to --retries <n> times (new flag on fetch and extract, default 2, 0 sends once).
  • Delay. Retry-After in whole seconds, capped at 30 s. Otherwise full jitter: uniform(0.25 s, min(30 s, 1 s × 2^attempt)), with attempt the 0-based retry index (first retry ≤ 1 s, second ≤ 2 s).
  • Budget. No retry starts when the time already spent plus the next delay would pass 90 s.
  • Never retried: the client timeout (--timeout; the API keeps working and may still charge a 200), any other 4xx, and a 429 whose body says the account is out of credits (it answers the same until the account changes). The EXTRACT_DOMAIN_NOT_ENABLED fallback to --autoparse is unchanged; a test pins that it spends no retries.
  • SERVICE_BUSY. A new error for 503 RESP009, with server_code, retry_after, a likely_cause that says the request was not charged (for Extract: the extraction service is still preparing this layout), and a next_action to retry the same command shortly without escalating.
  • Gateway budget. SERVER_BUDGET_MS is now 180 s (the whole request: auth, fetch, parse), not 90 s. The REQUEST_TIMEOUT error now says the API may still finish and charge the request, and suggests a --timeout above 180 s.
  • Shared helper. Retry-After parsing and the delay schedules move to src/core/backoff.ts. Crawl uses it with its own schedule unchanged (250 ms × 2^attempt, capped at 10 s, ±20%).
  • Docs. --retries in the fetch/extract help, the README Fetch section, and the protected-fetch and trace-debug skills.

Decision for the reviewer

The default client timeout stays at 120 s, below the gateway's 180 s budget. The old test pinned "client timeout above server budget" so the API could always answer with its own error. That no longer holds, and I replaced the test rather than raise the default, because changing the default timeout was out of scope. Raising it above 180 s (for example to 200 s) would restore that property. It is a one-line change if you want it here.

Testing

npm run typecheck && npm test: 402 tests, 0 failures (Node 26 locally; CI runs Node 20 and 22). New tests/fetch-retry.test.ts uses a stubbed fetch and an injected sleep and clock. It covers: jitter bounds, including on 503; Retry-After honoured and capped; the 90 s budget; no retry on 400/401/404/422 or on a timeout abort; the default of 2; --retries 0; flag validation; the unchanged autoparse fallback; and the SERVICE_BUSY shape for Fetch and Extract.

Not verified against the live API.

…es not fail the command

Fetch and Extract sent each request once. A 429, 502, 503 or 504 (none of
which the gateway charges) or a network error failed the command, and 503
RESP009 fell into the generic branch that advised the most expensive manual
configuration.

- Retry those answers up to --retries times (default 2; 0 sends once). Each
  wait honours Retry-After in whole seconds, capped at 30 s, or is full
  jitter: uniform(0.25 s, min(30 s, 1 s x 2^attempt)). No retry starts once
  the time spent plus the next wait would pass 90 s.
- Never retry the client timeout: the API keeps working and may still charge
  the request. Never retry other 4xx, or a 429 that says the account is out
  of credits. The Extract-not-enabled fallback to autoparse is unchanged.
- 503 RESP009 is now SERVICE_BUSY: not charged, retry shortly, no escalation.
- The gateway budget is 180 s for the whole request, not 90 s. The client
  timeout stays at 120 s; the timeout error now says the API may still
  finish and charge the request, and suggests a value above the budget.
- Retry-After parsing and the delay schedules move to src/core/backoff.ts;
  Crawl keeps its own schedule unchanged.

This branch has not been deployed

No deployments
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