Repository navigation
feat(fetch): retry uncharged busy answers so a brief capacity wait does not fail the command - #64
Draft
gonzalo-zenrows wants to merge 2 commits into
Draft
gonzalo-zenrows wants to merge 2 commits into
gonzalo-zenrows wants to merge 2 commits into
Conversation
This was referenced Oct 16, 2026
Draft
Author
|
Same retry policy across all clients (429/502/503/504 and pre-response connection errors; 2 retries by default; full jitter; Retry-After capped at 30 s; 90 s total budget; timeouts never retried):
|
…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.
…back to the command they ran The retry change resent requests the API may already be working on, retried pages the target site sent, and told agents to run a different command than the one that failed. - Retry a network error only when the request never reached the API (refused connection, DNS failure, connect timeout, unreachable network). A connection that closes after the request was sent is not retried: the API may still finish and charge it, as with a timeout. - Do not retry a 429, 502, 503 or 504 under original_status, or when allowed_status_codes covers it: that status may be the target site's page. - Decide a 429 by its error code: AUTH004 and AUTH014 are not retried, any other 429 is, including a non-JSON page. - Read Retry-After as whole digits clamped to 0.25-30 s, so 0 cannot drive a tight loop. Crawl keeps its own reading. --retries takes whole decimal digits only, so 1e1, 0x2 and padded values are usage errors. - Extract's Autoparse fallback shares the first request's 90 s budget. - Raise the default client timeout to 200 s, above the API's 180 s budget, as the public docs advise, and restore the test that guards it. - SERVICE_BUSY and the final error after a retried 429, 502, 503 or 504 suggest the command that ran, with its flags and extract method, and no longer advise the most expensive configuration. On Extract, SERVICE_BUSY quotes the API's own title, which says which of RESP009's two meanings applies. - --json results report attempts, the number of requests sent.
gonzalo-zenrows
force-pushed
the
feat/fetch-retry-jitter
branch
from
October 19, 2026 21:03
0df80be to
4104b09
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
zenrows fetchandzenrows extractsent each request once. A brief busy answer (429, 502, 503 or 504) failed the command, although Zenrows does not charge these answers. A 503RESP009told agents to rerun with--manual --js-render --premium-proxy. That is the most expensive configuration, and it does not help.This PR applies the retry policy that every Zenrows Fetch and Extract client shares.
What changes
--retries <n>times. The flag is new onfetchandextract. The default is 2, and0sends the request once.AUTH004orAUTH014means the account or key is out of credits, so the CLI sends it once. Every other 429 is retried, including a page that is not JSON.--original-status, or whenallowed_status_codeslists the status, the 429 or 5xx may be the target site's page. The CLI sends it once.Retry-Aftercounts when it is whole digits. The CLI waits that many seconds, at least 0.25 s and at most 30 s. Otherwise it waitsuniform(0.25 s, min(30 s, 1 s × 2^attempt)), whereattemptcounts retries from 0.--timeoutrises from 120 s to 200 s. The API's own budget is 180 s, and the public docs tell clients to wait at least that long. A test again guards that the default stays above the budget.SERVICE_BUSY. A 503RESP009now returns this new error. It says the request was not charged. It quotes the API's own title, which tells the two Extract meanings apart. The next action says to wait and run the same command again.SERVICE_BUSYand the final error after a retried 429, 502, 503 or 504 suggest the command that ran. The suggestion keeps the extract method and every flag, quoted for a shell. These errors no longer advise--js-renderor--premium-proxy.--json, the result has a newattemptsfield. It counts the HTTP requests the command sent, so 1 means no retry.src/core/backoff.tsholds the delay schedules. Crawl keeps its own schedule and its own reading ofRetry-After, so its behaviour does not change.fetchandextracthelp, the README Fetch section, and theprotected-fetchandtrace-debugskills describe all of the above.Behaviour change
Agents that branched on
FETCH_FAILEDfor a 503RESP009now seeSERVICE_BUSY. A command that hits a busy API now takes longer before it fails, but no retry starts after 90 s. A slow request now waits up to 200 s before the CLI gives up, instead of 120 s. A connection that closes after the request was sent now fails at once instead of being retried. The--jsonresult gains anattemptsfield, and no existing field changes. Exit codes stay the same.Not covered
A 429 page that is not JSON and mentions "upgrade" or "credit" is now retried. After the retries, the existing quota check still reports it as
POLICY_MAX_CREDITS_EXCEEDED. That check also serveszenrows usage, so changing it belongs in a separate PR. Under--original-status, aRESP009from Zenrows itself is now sent once, because the CLI cannot always tell it from a target page. Error envelopes other than--jsonresults do not report the attempt count. The CLI has no cancel option of its own; Ctrl-C ends the command, including a wait between retries. I did not run any of this against the live API.Testing
npm ci,npm run typecheckandnpm testpass: 505 tests, 0 failures, on Node 26 locally. CI runs Node 20 and 22.tests/fetch-retry.test.tsholds the shared conformance tables forRetry-After, jitter bounds and the retry decision. It also checks the 90 s budget on both paths and exact waits with a fixed random source.node:httpsockets. A refused port gets three attempts. A server that closes the connection after reading the request gets one.tests/fetch-retry-command.test.tsruns thefetchandextractcommands. It checks the suggested command for each extract method, the Autoparse fallback and a manual fetch. It also checksextract --retries 0and theattemptsfield.RESP009made three requests and suggested the exact command. A connection closed after the request made one request.