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-47 → syncArtificialAnalysisCatalog → 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):
- 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).
- Cap pagination at 50 pages. If
has_more is still true after that, throw 502 'Artificial Analysis pagination exceeded 50 pages; retry sync'.
- 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
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 barefetchand awhile (true):AbortSignal.timeout/fetchWithTimeouton the request (:195-197). A hung HTTPS connection never settles.pagination.has_morestays true, the loop never exits (:211-212).POST /sync-aainserver/routes/modelComparison.js:45-47→syncArtificialAnalysisCatalog→ this fetch. The clientrequest()(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 ahas_more: trueforever payload.This is the same class of defect
fetchWithTimeout(server/lib/fetchWithTimeout.js) andwithAbortTimeoutwere 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):
AbortSignal.timeout(15_000)(orfetchWithTimeout(..., 15000)). Map abort to the existing 502ServerErrorused for upstream failures (artificialAnalysis.js:199).has_moreis still true after that, throw 502'Artificial Analysis pagination exceeded 50 pages; retry sync'.has_more: truestub stops at 50 and throws rather than looping.model:lightbecause this is a well-specified edit in one module plus its existing test file.effort:lowbecause the timeout helper and 502 mapping already exist; do not derive effort from the light model tier.Scope: small
Acceptance criteria
has_more: truestops at 50 pages and throws 502.artificialAnalysis.test.jsexisting case).