Skip to content

fix(find): survive slow/broken network paths to Raster (#97) - #98

Merged
moskovich merged 2 commits into
mainfrom
fix/97-find-network-resilience
Jul 28, 2026
Merged

fix(find): survive slow/broken network paths to Raster (#97)#98
moskovich merged 2 commits into
mainfrom
fix/97-find-network-resilience

Conversation

@moskovich

Copy link
Copy Markdown
Contributor

Fixes #97.

Root cause

Node's happy-eyeballs (autoSelectFamily) gives each address-family connect attempt 250 ms by default. api.raster.art resolves 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=net trace:

connect/multiple: attempting to connect to 65.109.41.181:443 (addressType: 4)
connect/multiple: setting the attempt timeout to 250 ms
connect/multiple: connection to 65.109.41.181:443 timed out
connect/multiple: attempting to connect to 2a01:4f9:c01e:76f::1:443 (addressType: 6)
→ Error: fetch failed

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)

  1. Entry (index.ts): setDefaultAutoSelectFamilyAttemptTimeout(2500) — a transcontinental IPv4 connect no longer races the speed of light.
  2. raster-client: every query carries AbortSignal.timeout(20s), so a stalled request becomes a fast error instead of a silent hang; network failures rethrow as RasterUnreachableError naming the endpoint and the undici cause (bare fetch failed names neither).
  3. find: when Raster is unreachable at the coords step, degrade to a one-item playlist with a warning instead of dying — Raster enriches a find; the coords in hand are enough to build something playable.

Verification

  • The find: hangs indefinitely on canonical feralfile.com series URL #97 repro (find on the 36 Points Display Edition series URL, previously killed at 90 s and 150 s with no output) now completes: 35 items.
  • New tests: unreachable → typed error with endpoint + cause; abort → typed error; every raster fetch carries an abort signal.
  • Suite: 447/447, prettier clean.

Noted, out of scope

find on 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.

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 feralfile-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewers: 2 (approve: 0, comment: 0, request-changes: 2).

Critical issues

  • rasterQuery only converts failures from fetch() itself. A response body that stalls or terminates after headers causes response.text() or response.json() to reject outside that boundary, leaving resolveCoords unable to recognize RasterUnreachableError and 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 fetch returns a Response, but its body read rejects with an abort/network error; also verify the find flow 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.
moskovich added a commit that referenced this pull request Jul 27, 2026
…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).
@moskovich

Copy link
Copy Markdown
Contributor Author

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 feralfile-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@moskovich
moskovich merged commit 90d4513 into main Jul 28, 2026
6 checks passed
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.

find: hangs indefinitely on canonical feralfile.com series URL

2 participants