Skip to content

fix(miner): orb-export.js's sendAmsExportBatch fetch has no AbortSignal.timeout #7237

Description

@JSONbored

Context

packages/loopover-miner/lib/orb-export.js's sendAmsExportBatch (lines 180-201) POSTs the anonymized
telemetry batch straight to the AMS collector with no request timeout at all:

export async function sendAmsExportBatch({ batch, secret, collectorUrl = resolveAmsCollectorUrl(), collectorToken, fetchFn = fetch }) {
  if (!Array.isArray(batch) || batch.length === 0) return { sent: 0 };
  const instanceId = amsInstanceId(secret);
  const body = JSON.stringify({ instanceId, events: batch });
  const signature = createHmac("sha256", secret).update(body).digest("hex");
  try {
    const res = await fetchFn(collectorUrl, {
      method: "POST",
      headers: { /* ... */ },
      body,
    });
    if (!res.ok) return { sent: 0, error: `http_${res.status}` };
  } catch (error) {
    return { sent: 0, error: describeCliError(error) };
  }
  return { sent: batch.length };
}

There is no AbortSignal.timeout(...) (or equivalent AbortController) anywhere in this call — a stalled
TCP connection or a collector that accepts the connection but never responds will hang this fetchFn call
forever. sendAmsExportBatch is invoked synchronously (awaited) from runOrbExportCli's --send path
(packages/loopover-miner/lib/orb-export.js, runOrbExportCli), so a hang here hangs the entire
loopover-miner orb export --enable --send CLI invocation, including any unattended/scheduled use of it.

Every other external-fetch adapter in this package already bounds its request with a timeout:

  • lib/http-retry.js's fetchWithRetry (fetchOnce) always issues a fresh AbortSignal.timeout(timeoutMs)
    per attempt — used by opportunity-fanout.js's githubGetJson and discovery-index-client.js's
    queryDiscoveryIndex/submitSoftClaim.
  • lib/live-issue-snapshot.js's fetchLiveIssueSnapshot passes signal: AbortSignal.timeout(requestTimeoutMs)
    directly.
  • lib/update-check.js's fetchLatestPackageVersion uses a manual AbortController + setTimeout(() => controller.abort(), ...).

sendAmsExportBatch is the one external-fetch call site in this package with no bound at all — the same
caliber of gap already fixed for contribution-profile-extract.js, self-review-context.js's
fetchRepoFocusManifestFile, and the extension's screenshot fetch (fetchShotContentBlock) in this exact
gap-audit series, none of which cover orb-export.js.

Requirements

  • sendAmsExportBatch in packages/loopover-miner/lib/orb-export.js must bound its fetchFn(collectorUrl, ...) call with signal: AbortSignal.timeout(timeoutMs), added to the existing init object passed to
    fetchFn (do not change the method/headers/body fields already there).
  • Add a timeoutMs parameter to sendAmsExportBatch's destructured options object, defaulting to a new
    exported constant DEFAULT_ORB_EXPORT_TIMEOUT_MS (module-level const, value 10_000, matching this
    package's other default request timeouts in live-issue-snapshot.js/opportunity-fanout.js).
    runOrbExportCli does not need to pass timeoutMs explicitly — the default must apply automatically to
    every real (non-test) invocation.
  • A timeout must be caught by the existing try { ... } catch (error) { return { sent: 0, error: describeCliError(error) }; } block exactly like any other thrown fetch error — no new catch branch is
    needed, AbortSignal.timeout's abort surfaces as a thrown DOMException/AbortError that the existing
    catch already handles.
  • Do not add retry logic (fetchWithRetry) — this is strictly about bounding the single request with a
    timeout, matching the narrower fix already applied to the other single-fetch adapters named above (e.g.
    live-issue-snapshot.js), not the retrying adapters (http-retry.js's consumers).

Deliverables

  • sendAmsExportBatch passes signal: AbortSignal.timeout(timeoutMs) on its fetchFn call.
  • A new exported DEFAULT_ORB_EXPORT_TIMEOUT_MS constant (10_000) is the default when the caller does
    not supply timeoutMs.
  • A regression test asserting that a fetchFn stub which never resolves (or an injected already-aborted
    signal / a fetchFn that rejects with an AbortError) causes sendAmsExportBatch to resolve with
    { sent: 0, error: ... } rather than hanging, and that the real default timeout value is exercised
    (e.g. asserting the init.signal passed to a stubbed fetchFn is an AbortSignal).

Test Coverage Requirements

This repo's Codecov patch gate (codecov/patch) enforces target: 99%, threshold: 0%, branch-counted, on
every changed line/branch under src/**/packages/** — this fix is inside packages/** and is gated. Add
the regression test described above so the new timeout wiring is exercised, not just the existing success/
non-2xx/thrown-error branches. Measure locally with npm run test:coverage (unsharded).

Expected Outcome

sendAmsExportBatch (and therefore loopover-miner orb export --enable --send) can no longer hang
indefinitely on a stalled or non-responding AMS collector connection — a request that exceeds
DEFAULT_ORB_EXPORT_TIMEOUT_MS (or a caller-supplied timeoutMs) resolves with { sent: 0, error: ... }
the same way any other transport failure already does, bringing this adapter in line with every other
external-fetch call site in the package.

Links & Resources

  • packages/loopover-miner/lib/orb-export.js (the file to fix — sendAmsExportBatch, lines ~180-201)
  • packages/loopover-miner/lib/http-retry.js's fetchOnce and packages/loopover-miner/lib/live-issue-snapshot.js's fetchLiveIssueSnapshot for the established AbortSignal.timeout pattern to mirror
  • packages/loopover-miner/lib/update-check.js's fetchLatestPackageVersion for the AbortController variant of the same discipline

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions