Skip to content

[resilience-aa-catalog-fetch-timeout] Bound Artificial Analysis catalog sync with a request timeout and page ceiling #6940

Description

@atomantic

Slice

Failure-path audit of outbound I/O timeouts (Artificial Analysis catalog sync). Audited 2026-09-11.

Problem

fetchAllArtificialAnalysisModels (server/services/artificialAnalysis.js:189-214) paginates the paid Artificial Analysis API with a bare fetch and a while (true):

  • No AbortSignal.timeout / fetchWithTimeout on the request (:195-197). A hung HTTPS connection never settles.
  • No page ceiling. If pagination.has_more stays true, the loop never exits (:211-212).
  • The user-facing route awaits the whole sync: POST /sync-aa in server/routes/modelComparison.js:45-47syncArtificialAnalysisCatalog → this fetch. The client request() (client/src/services/apiCore.js:67) has no timeout either, so the comparison-page Sync button spins until the tab is closed.

Existing tests (server/services/artificialAnalysis.test.js:153-191) cover happy-path pagination and a version-change abort. They do not cover a hung fetch or a has_more: true forever payload.

This is the same class of defect fetchWithTimeout (server/lib/fetchWithTimeout.js) and withAbortTimeout were added to stop on other outbound calls.

Impact

Runtime. A stalled or lying Artificial Analysis API holds an Express request and a UI action open indefinitely. Optional feature, but the failure is a user-triggered hang with no recovery short of restart/reload. Unbounded pagination can also grow memory without bound.

Fix

Single-file, decided numbers (do not bikeshed):

  1. Each page request uses AbortSignal.timeout(15_000) (or fetchWithTimeout(..., 15000)). Map abort to the existing 502 ServerError used for upstream failures (artificialAnalysis.js:199).
  2. Cap pagination at 50 pages. If has_more is still true after that, throw 502 'Artificial Analysis pagination exceeded 50 pages; retry sync'.
  3. Tests: a fetch that never resolves rejects with a timeout/502; a has_more: true stub stops at 50 and throws rather than looping.

model:light because this is a well-specified edit in one module plus its existing test file. effort:low because the timeout helper and 502 mapping already exist; do not derive effort from the light model tier.

Scope: small

Acceptance criteria

  • A hung Artificial Analysis page request fails within ~15s with a 502, not a pending promise.
  • A payload that always sets has_more: true stops at 50 pages and throws 502.
  • Happy-path two-page pagination still returns both pages (artificialAnalysis.test.js existing case).
  • The API key is not logged or copied into the error message (it stays a request header only).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

effort:lowLow reasoning budget per stepmodel:lightMechanical single-file changeplanTracked by /do:replanplanner:grok-4-6Plan authored by the grok-4-6 modelresilienceProposed from a failure-path audit

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions