Repository navigation
feat: classify transient fetch failures on failed crawl results - #1342
FagnerMartinsBrack wants to merge 2 commits into
Conversation
In the week to 2026-11-07, 637 crawl-transport records remained. Later slices report them by failure class and retry only where the evidence gates allow, so every worker needs one classifier. - decideFetchFailure classifies a fetch failure by known codes and typed deadline scope as retryable (local or durable), terminal or cancelled, keeping the original cause. Generic "fetch failed" or "503" strings stay terminal. - A failed crawl result carries that decision whenever the conditional GET threw, and passes it through crawlAndFinalizeArticle. - fetchH2 rejects a stream that ends without a response with an error coded H2_STREAM_ENDED_WITHOUT_RESPONSE, so a closed socket is transient on every direct leg. Log severity, fetch deadlines, queue visibility and receive counts are unchanged; they follow in later slices. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QeLuXXwkPCA9Bz167XYffk
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 35s —— View job I'll analyze this and get back to you. |
High Priority Issues (Must Fix)None found Medium Priority Issues1. A primary-leg 403 followed by resets is classified as durable-retryable
In A 403 on the primary leg followed by resets on the h2 and curl legs is the usual pattern for an edge that blocks by fingerprint, which the canary section in CLAUDE.md describes (Cloudflare TLS, Fastly JA3). If those saves become durable retries, each retry will spend a full ladder budget against an origin that is blocking us, and the save will still fail. This slice doesn't change any behaviour, so nothing breaks today. Still, the test states the outcome as intended. Before slice 2 consumes the decision, choose one of these:
Low Priority Suggestions1.
|
|
@claude Fix the review issues. Use your Read tool to read the local file Context
Review to AddressSee the review comment above this one for the issues to fix. |
|
Claude finished @FagnerMartinsBrack's task in 18m 19s —— View job Fixed the medium-priority review issue
What was wrong: when a leg got back a block status such as 403, What changed: the ladder now records the failure instead of losing it, which was the first option in the review.
Not done: I left both low-priority suggestions alone (the |
An escalated block status (403, 429, WAF challenge) followed by resets was classified as a durable retry because the ladder dropped the status. The ladder failure now records escalated statuses and the every-leg rule treats any of them as terminal. Co-authored-by: Fayner Brack <FagnerMartinsBrack@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 26s —— View job I'll analyze this and get back to you. |
High Priority Issues (Must Fix)None found Medium Priority IssuesNone found Low Priority Suggestions1. A 429 escalation now makes the whole ladder terminal, which goes against how the ladder treats a 429
Nothing consumes 2.
|
Slice 1 (step 1) of the transient-crawl retry plan: the fetch-failure classifier and nothing else. It changes no retry behaviour, no log line, no deadline and no queue setting.
What changed
decideFetchFailure(src/packages/crawl-article/src/fetch-retry-decision.ts) classifies a thrown fetch failure asretryable(local or durable),terminalorcancelled, keeping the original cause.ECONNRESET,UND_ERR_SOCKET,UND_ERR_CLOSED,H2_STREAM_ENDED_WITHOUT_RESPONSE, and curl exits 52 and 56.TimeoutError) is durable.cancelled, unless its reason is a deadline, in which case it is durable.EBUSY, curl exit 35, block-class errors, bare "fetch failed" and "503"-in-message stay terminal.TransportLadderFailureis read across itspreviousErrorsandcause, following each leg's cause chain and recursing into nested ladders. It is durable only when every recorded leg was transient or timed out; one non-transient leg makes it terminal. This does not gate onretryDisposition, which is left unchanged.fetchH2now rejects a stream that ended without a response with an unexportedH2StreamEndedWithoutResponsewhosecodeisH2_STREAM_ENDED_WITHOUT_RESPONSE. The message is unchanged.decisionfield.decision: FetchRetryDecision | undefinedis now required on thefailedvariants ofCrawlArticleResult, of the internalFetchOutcomeand ofCrawlAndFinalizeResult.crawlAndFinalizeArticle.undefined.Tests
fetch-retry-decision.test.ts:runTransportLadderending on curl exit 56 after transient direct legs;initCrawlFetchdirect ladder against anode:netserver that callssocket.destroy()(curl faked as exit 52), which is durable. It uses undici'sfetch, because jest's sandboxedfetchdrops the socket error code.h2-fetch.test.ts: thecodeon the clean-close case, plus a destroyed-socket server.crawl-article.test.ts: an all-legsECONNRESETcrawl gives a durable decision, and a 503 givesdecision: undefined.crawl-and-finalize-article.test.ts:decisionpasses through unchanged.pnpm checkpasses (61 projects), and the pre-commit hook passed.Notes for review
instanceof Error, so errors from another realm (as under jest) still classify.CLAUDE.mdsays to commit tomain, but this session was set to push torp/confident-einstein-e4cxtl, so this lands as a draft PR. The plan's question of one PR per slice is still for the owner to answer.Generated by Claude Code