fix(find): survive slow/broken network paths to Raster (#97) - #98
Conversation
Three layers, one root cause. Node's happy-eyeballs gives each address-family connect attempt 250 ms — shorter than one round trip to api.raster.art from Asia (~250 ms RTT). On networks with broken IPv6 the IPv4 attempt dies mid-handshake, the IPv6 fallback has no route, and find fails or stalls with no output. - entry: raise the auto-select-family attempt timeout to 2.5 s so a transcontinental IPv4 connect is not cut off mid-handshake - raster-client: 20 s AbortSignal on every query — a stalled request becomes a fast error, never a silent hang; network failures rethrow as RasterUnreachableError naming the endpoint and the undici cause (bare 'fetch failed' says neither) - find: Raster unreachable at the coords step degrades to a one-item playlist with a warning instead of dying — Raster enriches a find, it is not required to build something playable Repro from #97 (FF series URL that hung at 90 s and 150 s) now completes: 35 items, or a fast actionable error when the network is truly down. Suite: 447/447.
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 2 (approve: 0, comment: 0, request-changes: 2).
Critical issues
rasterQueryonly converts failures fromfetch()itself. A response body that stalls or terminates after headers causesresponse.text()orresponse.json()to reject outside that boundary, leavingresolveCoordsunable to recognizeRasterUnreachableErrorand perform its promised single-token fallback. Keep response consumption within the network-error conversion boundary while preserving intentional HTTP/GraphQL errors. This was independently identified by both reviewers.
Missing tests
- Add coverage where
fetchreturns aResponse, but its body read rejects with an abort/network error; also verify thefindflow degrades to a one-item playlist.
Both reviewers caught it: rasterQuery only converted failures from fetch() itself. A body that stalls or terminates after headers rejects in response.text()/response.json(), outside the boundary — so resolveCoords couldn't recognize RasterUnreachableError and never performed its promised single-token fallback. Body consumption now sits inside the boundary: a rejected read on an ok response throws RasterUnreachableError (a 200 whose body can't be read is unusable either way; falling back is right even for server-side garbage). Intentional API-level errors keep their types: an HTTP error stays a status error even when its body is unreadable (best-effort text, never masks the status), and GraphQL errors are unchanged. Tests: mid-stream termination → typed error; 502 with unreadable body → status error, not unreachable; end-to-end find run against a mock server that destroys the body → warns and degrades to a one-item playlist. 450/450.
…arketplace→Raster A Feral File series URL resolved ONE token via ff-marketplace, then asked Raster, which maps the token to its parent artwork — so a single-edition series page (e.g. 36 Points Display Edition) built the sibling collection's 35 tokens instead of the 1 the page describes. Flagged as out-of-scope in #98; this is that fix. Series URLs now route through source-resolver's Feral File site module (series slug → /api/series → /api/artworks?seriesID), exactly like shows already did — one FF resolution path for page-shaped URLs, and find builds precisely the tokens the page describes. Artwork URLs keep the coords→Raster path: a single-token page carries no series intent to preserve. Verified live: the Display Edition series URL now builds 1 item with correct provenance (was 35), in seconds (was minutes of indexer warming for tokens the user never asked about).
|
Both findings addressed in 35b26f1: body consumption now sits inside the network-error boundary — a stalled/terminated body on an ok response throws RasterUnreachableError so the single-token fallback stays reachable, while HTTP-status errors keep their type even with an unreadable body (best-effort text, never masks the status). Tests: mid-stream termination → typed error; 502 + unreadable body → status error; and an end-to-end find run against a mock server that destroys the body mid-stream, asserting the warn + one-item degrade. 450/450. |
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 2 (approve: 2, request-changes: 0).
No meaningful findings after re-review. The network-error handling correctly covers both fetch() failures and response-body failures, and the single-token fallback is limited to paths where coordinates are available.
Fixes #97.
Root cause
Node's happy-eyeballs (
autoSelectFamily) gives each address-family connect attempt 250 ms by default.api.raster.artresolves to one IPv4 (Hetzner FI) and one IPv6 address; from Asia the IPv4 RTT is ~250 ms, so the IPv4 attempt is cut off mid-handshake, the IPv6 fallback has no route on v6-broken networks, and the fetch fails — or, when TCP retries stack, stalls with no output at all.NODE_DEBUG=nettrace:The same command succeeded in isolation when one SYN happened to land under 250 ms — which is why the failure looked random.
Fix (three layers)
index.ts):setDefaultAutoSelectFamilyAttemptTimeout(2500)— a transcontinental IPv4 connect no longer races the speed of light.AbortSignal.timeout(20s), so a stalled request becomes a fast error instead of a silent hang; network failures rethrow asRasterUnreachableErrornaming the endpoint and the undicicause(barefetch failednames neither).Verification
findon the 36 Points Display Edition series URL, previously killed at 90 s and 150 s with no output) now completes: 35 items.Noted, out of scope
findon that 1-edition FF series URL expands via Raster to the parent artwork's 35 tokens. Defensible (Raster is the series authority) but arguably surprising for a single-edition series page; flagging rather than changing semantics here.