Skip to content

feat(find): shared HTTP deadlines everywhere + visible indexer progress - #101

Merged
moskovich merged 8 commits into
mainfrom
feat/find-network-hardening
Jul 28, 2026
Merged

feat(find): shared HTTP deadlines everywhere + visible indexer progress#101
moskovich merged 8 commits into
mainfrom
feat/find-network-hardening

Conversation

@moskovich

Copy link
Copy Markdown
Contributor

Stacked on #100 (which stacks on #98) — merge in order. Completes the hardening #98 started.

1. Shared HTTP deadlines (no fetch can hang the CLI)

#98 fixed timeouts for Raster only. The same bare-fetch hazard existed at 15 more call sites: Art Blocks, Feral File, fxhash, Neort, Objkt, OpenSea, Verse, playlist-source, the FF indexer (3), and the feed fetcher (3). All now route through one fetchWithTimeout (20 s default); a stalled connect or unanswered request is structurally incapable of hanging the CLI rather than being per-call-site discipline. Caller-supplied abort signals are composed with the deadline (AbortSignal.any), never replaced.

2. Indexer progress is visible

The FF indexer batch loop always had progress logging — logger.info, which is verbose-gated, so warming a series of unseen tokens (minutes by design) read as a dead terminal in normal runs. This was most of the perceived "hang" in #97 once the network layer was fixed. getNFTTokenInfoBatch gains an optional onProgress callback (best-effort, exceptions swallowed); find renders N/M tokens indexed... per batch and shows the --limit 5 warming hint before indexing large sets (it previously appeared only after a failure).

Tests

449/449. New: deadline signal present on every wrapped fetch; caller signal composed-not-replaced (abort propagates through AbortSignal.any).

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: 3 (approve: 0, comment: 0, request-changes: 3).

Critical issues

  • src/utilities/ssh-access.ts:103 still sends the SSH access request with bare fetch, and its preceding assertFF1CommandCompatibility probe defaults to bare fetch as well. Either stalled request can leave ff-cli ssh waiting indefinitely, contradicting the PR’s shared-deadline / “no fetch can hang the CLI” guarantee. Route both device requests through the shared timeout mechanism, and add coverage that verifies the SSH request receives a deadline signal. This was independently identified by all three reviewers.

@feralfile-bot

Copy link
Copy Markdown
Contributor

Automated human review request for PR #101.

Risk zone: red
Responsibility area: ff-cli network layer + find/resolution flow
Why this needs human review: The PR applies a cross-cutting runtime-reliability change across outbound resolver/indexer calls and alters failure semantics (find can now continue with degraded data), which can affect broad user-facing command behavior and deserves steward confirmation at first appearance.

This PR needs targeted human review on the scoped areas below, not a full second pass of the whole PR.

  • @jollyjoker992: please review Review global network behavior introduced by index.ts (Node setDefaultAutoSelectFamilyAttemptTimeout) and shared timeout/cancellation changes in src/utilities/http.ts, plus the new find fallback path for unreachable Raster and series resolution routing in src/commands/find.ts.. Context: FF operator CLI (NFT/collection → playlist build) (Hieu Pham).

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).
Completes the #97 hardening beyond Raster. Every resolver-facing fetch
(Art Blocks, Feral File, fxhash, Neort, Objkt, OpenSea, Verse,
playlist-source, nft-indexer, feed-fetcher — 15 call sites) now goes
through fetchWithTimeout, so a stalled connect or unanswered request is
structurally incapable of hanging the CLI. Caller-supplied signals are
composed with the deadline via AbortSignal.any, never replaced.

The FF indexer batch loop already logged progress — verbose-gated, so
a multi-minute warm of unseen tokens rendered as a silent hang in
normal runs. getNFTTokenInfoBatch gains an optional onProgress
callback; find renders per-batch progress and surfaces the --limit
warming hint upfront (it used to appear only after a failure).

Suite: 449/449.
…ed deadline

All three reviewers caught the gap: ssh-access.ts sent the device
request with bare fetch, and assertFF1CommandCompatibility's default
probe did too — either could hang `ff-cli ssh` indefinitely,
contradicting this PR's no-fetch-can-hang guarantee. Both now route
through fetchWithTimeout; injected fetchFn overrides are unchanged.

Tests: ssh request and default probe each carry a deadline signal.
454/454.
@moskovich
moskovich force-pushed the feat/find-network-hardening branch from 9ff62bd to 2107193 Compare July 27, 2026 13:10
@moskovich

Copy link
Copy Markdown
Contributor Author

Addressed in e700727's follow-up commit: ssh-access.ts and assertFF1CommandCompatibility's default probe both route through fetchWithTimeout now (injected fetchFn overrides unchanged), with tests asserting each device request carries a deadline signal. Branch also rebased onto the updated #100/#98 stack. 454/454.

@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: 3 (approve: 1, comment: 1, request-changes: 1).

  • Medium — find suppresses the final progress callback (done === total), so a one-batch index—including the recommended --limit 5 warm-up—shows no N/M tokens indexed... output, while multi-batch runs never show completion. This was independently identified by two reviewers and defeats the visible-progress goal for the most common small runs. Render the final callback (or emit batch-start progress), and add coverage for single- and multi-batch progress output.

The test called sendSshAccessCommand with an invented argument shape
and leaned on the developer's real ~/.config/ff-cli to resolve a
device — green on a configured machine, red in CI where the
config-resolution early-out returned before any fetch. Now it uses
the real signature, writes a temp config and points XDG_CONFIG_HOME
at it, asserts the command actually succeeded (so an early-out can't
fake a pass), and requires every device fetch — probe and request —
to carry a deadline signal.
Two reviewers caught it: suppressing the final callback meant a
one-batch index — including the recommended --limit 5 warm-up —
printed no progress at all, and multi-batch runs never showed
completion. The final N/N line IS the indexing-finished signal;
render unconditionally.

Coverage: single batch → [1/1]; 12 tokens → [10/12, 12/12]; a
throwing progress renderer never breaks the batch. 457/457.
@moskovich

Copy link
Copy Markdown
Contributor Author

Progress finding addressed: the callback renders unconditionally now — the final N/N line is the completion signal, so single-batch runs (including the --limit 5 warm-up) show progress too. Coverage added for single-batch [1/1], multi-batch [10/12, 12/12], and a throwing renderer never breaking the batch. Also fixed this cycle: the ssh deadline test now carries its own temp device config (XDG_CONFIG_HOME), which is what CI red was — the original test leaned on the developer's real ~/.config/ff-cli and asserted nothing when device resolution early-outed. 457/457.

@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: 3 (approve: 0, comment: 0, request-changes: 3).

Critical issues

  • The shared-deadline goal remains incomplete. Several production defaults still use bare globalThis.fetch: feral-file-artwork.ts, ff1-device.ts, ff1-relayer.ts, and handoff-client.ts. A stalled artwork lookup, device cast/relayer request, or handoff-broker poll can therefore still hang the CLI. This is a recurring concern: the follow-up correctly covered SSH access and the compatibility probe, but these remaining paths still contradict the PR’s “no fetch can hang” / “everywhere” claim. Route their default implementations through fetchWithTimeout while retaining injectable fetch functions.

Medium issues

  • find.ts suppresses the callback when done === total. Since the indexer reports only after a batch completes, a one-batch run (including the suggested --limit 5 warm-up) emits no N/N tokens indexed... line, and multi-batch runs omit the final batch. Render the callback unconditionally (or emit an initial progress line).

  • fetchWithTimeout composes only init.signal. For a Request input that carries its own signal, supplying the timeout signal replaces that cancellation rather than composing it, contrary to the helper’s stated contract. Compose init.signal ?? input.signal when input is a Request.

Missing tests

  • Cover deadlines for the remaining default-fetch paths, especially device casting, relayer delivery, handoff polling, and Feral File artwork resolution.
  • Assert progress output for a single-batch run and the final batch of a multi-batch run.
  • Assert that a signal attached to a Request input propagates through fetchWithTimeout.

@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: 3 (approve: 0, request-changes: 3).

Critical issues

  • Shared deadlines still do not cover several production fetch defaults, contradicting the PR’s “no fetch can hang the CLI” goal. sendPlaylistToDevice defaults to bare globalThis.fetch and passes it to both the compatibility probe and /api/cast request (src/utilities/ff1-device.ts:196,221,265); the relayer fallback, handoff broker, and Feral File artwork resolver likewise default to bare fetch (src/utilities/ff1-relayer.ts:42, src/utilities/handoff-client.ts:112, src/utilities/feral-file-artwork.ts:40). A stalled response can therefore still hang play, find --play, pairing, relayer delivery, or Feral File artwork resolution. Route production defaults through fetchWithTimeout while preserving injectable test fetches. This was independently identified across all three reports (with the primary device path raised by two).

Medium issues

  • fetchWithTimeout preserves only init.signal; for a Request input without init.signal, it replaces the request’s existing abort signal with the deadline (src/utilities/http.ts:17-23). Compose input.signal as well, so supported Request callers retain cancellation semantics alongside the timeout.

Missing tests

  • Add focused coverage for the default sendPlaylistToDevice probe and cast paths, relayer/artwork defaults as applicable, and a Request input whose own signal aborts the composed request.

…st signals compose

Round-2 findings from all three reviewers, all valid:

- ff1-device (probe + /api/cast), ff1-relayer, handoff-client, and
  feral-file-artwork all defaulted their injectable fetch to bare
  globalThis.fetch — so play, find --play, pairing, relayer delivery,
  and FF artwork resolution could still hang. All five defaults now
  use defaultDeadlineFetch (fetchWithTimeout with the standard
  deadline); injected test fetches are untouched.

- fetchWithTimeout replaced a Request input's own abort signal with
  the deadline when init.signal was absent (init wins over the
  request's signal in fetch). The Request's signal is now composed
  via AbortSignal.any alongside the deadline.

Coverage: defaultDeadlineFetch attaches a deadline; artwork resolver
and relayer defaults carry one; a Request input's own abort
propagates through the composed signal. 461/461.
@moskovich

Copy link
Copy Markdown
Contributor Author

Round 2 addressed: ff1-device (probe and cast), ff1-relayer, handoff-client, and feral-file-artwork production defaults all route through defaultDeadlineFetch now — injected test fetches untouched — and fetchWithTimeout composes a Request input's own signal with the deadline via AbortSignal.any instead of replacing it. Tests cover the defaults and the Request-signal propagation. 461/461.

@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: 3 (approve: 3, comment: 0, request-changes: 0).

No meaningful findings remain after consolidation. The discussion confirms prior timeout, progress-rendering, test-isolation, and Request-signal composition concerns were addressed in follow-up commits.

@moskovich
moskovich merged commit 4577fc2 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.

2 participants