Context
src/selfhost/qdrant-vectorize.ts implements a self-host Vectorize adapter backed by an operator's own Qdrant REST API (initQdrantCollection, and the upsert/query/deleteByIds methods of createQdrantVectorize). Every one of its 5 fetch() calls has no AbortSignal.timeout(...):
initQdrantCollection — line 72 (PUT .../collections/{collection}) and line 81 (GET .../collections/{collection})
upsert — line 102 (PUT .../points)
query — line 122 (POST .../points/search)
deleteByIds — line 149 (POST .../points/delete)
This is a self-host-only Node.js adapter (unlike the Cloudflare Workers runtime, which has its own platform-level subrequest ceiling), so an unresponsive or network-partitioned Qdrant instance can hang these calls indefinitely in the review/RAG pipeline's request-handling process. Every other external-HTTP self-host/review adapter in this audit's scope uses a bounded AbortSignal.timeout(...): src/selfhost/ai.ts (AbortSignal.timeout(120_000) on its embeddings/chat-completions/messages calls), src/review/visual/actions-fallback.ts (AbortSignal.timeout(DEFAULT_TIMEOUT_MS)), src/review/enrichment-wire.ts, src/review/alerts.ts, src/review/visual/capture.ts. qdrant-vectorize.ts is the one external-fetch adapter in src/selfhost/** with no timeout on any of its calls.
Note that query's fetch is already wrapped in a try/catch that degrades to { matches: [] } on any thrown error (so a rejected/aborted fetch there is already handled gracefully) — the gap is specifically the absence of any time bound, not missing error handling.
Requirements
- Every
fetch() call in src/selfhost/qdrant-vectorize.ts (initQdrantCollection's two calls, and upsert/query/deleteByIds) must pass an AbortSignal.timeout(...).
- Introduce one named timeout constant at module scope (e.g.
QDRANT_FETCH_TIMEOUT_MS), configurable via a sensible default (recommend 10-15 seconds, consistent with this file's other self-host-adapter siblings' order of magnitude) — a single shared constant for all 5 call sites is sufficient; this does not need to be separately configurable per operation.
upsert and deleteByIds currently throw a plain Error on any non-ok response; a timeout on either must continue to surface as a thrown error there (an AbortSignal.timeout rejection propagates out of the await fetch(...) call itself, so no new catch/throw logic is needed beyond adding the signal).
query's existing try { ... } catch { return { matches: [] } } around its fetch must continue to degrade to { matches: [] } on a timeout exactly as it already does for any other network failure (no behavior change needed there beyond adding the signal).
initQdrantCollection's two calls must continue to throw on failure exactly as today (a timeout naturally propagates as a rejected fetch, causing the existing throw paths to fire).
Deliverables
Test Coverage Requirements
src/selfhost/qdrant-vectorize.ts is under src/**, measured by Codecov. This PR must hit 99%+ patch coverage on every changed line and branch, including the timeout paths exercised by the regression tests above.
Expected Outcome
Every Qdrant REST call in src/selfhost/qdrant-vectorize.ts is bounded by an explicit timeout, consistent with every other external-HTTP self-host adapter in this codebase, so an unresponsive or network-partitioned Qdrant instance can no longer hang the self-host review/RAG request-handling process.
Links & Resources
src/selfhost/qdrant-vectorize.ts (initQdrantCollection, lines 66-89; createQdrantVectorize, lines 92-163)
- Sibling timeout convention:
src/selfhost/ai.ts (AbortSignal.timeout(120_000))
Context
src/selfhost/qdrant-vectorize.tsimplements a self-host Vectorize adapter backed by an operator's own Qdrant REST API (initQdrantCollection, and theupsert/query/deleteByIdsmethods ofcreateQdrantVectorize). Every one of its 5fetch()calls has noAbortSignal.timeout(...):initQdrantCollection— line 72 (PUT .../collections/{collection}) and line 81 (GET .../collections/{collection})upsert— line 102 (PUT .../points)query— line 122 (POST .../points/search)deleteByIds— line 149 (POST .../points/delete)This is a self-host-only Node.js adapter (unlike the Cloudflare Workers runtime, which has its own platform-level subrequest ceiling), so an unresponsive or network-partitioned Qdrant instance can hang these calls indefinitely in the review/RAG pipeline's request-handling process. Every other external-HTTP self-host/review adapter in this audit's scope uses a bounded
AbortSignal.timeout(...):src/selfhost/ai.ts(AbortSignal.timeout(120_000)on its embeddings/chat-completions/messages calls),src/review/visual/actions-fallback.ts(AbortSignal.timeout(DEFAULT_TIMEOUT_MS)),src/review/enrichment-wire.ts,src/review/alerts.ts,src/review/visual/capture.ts.qdrant-vectorize.tsis the one external-fetch adapter insrc/selfhost/**with no timeout on any of its calls.Note that
query's fetch is already wrapped in atry/catchthat degrades to{ matches: [] }on any thrown error (so a rejected/aborted fetch there is already handled gracefully) — the gap is specifically the absence of any time bound, not missing error handling.Requirements
fetch()call insrc/selfhost/qdrant-vectorize.ts(initQdrantCollection's two calls, andupsert/query/deleteByIds) must pass anAbortSignal.timeout(...).QDRANT_FETCH_TIMEOUT_MS), configurable via a sensible default (recommend 10-15 seconds, consistent with this file's other self-host-adapter siblings' order of magnitude) — a single shared constant for all 5 call sites is sufficient; this does not need to be separately configurable per operation.upsertanddeleteByIdscurrently throw a plainErroron any non-ok response; a timeout on either must continue to surface as a thrown error there (anAbortSignal.timeoutrejection propagates out of theawait fetch(...)call itself, so no new catch/throw logic is needed beyond adding thesignal).query's existingtry { ... } catch { return { matches: [] } }around its fetch must continue to degrade to{ matches: [] }on a timeout exactly as it already does for any other network failure (no behavior change needed there beyond adding thesignal).initQdrantCollection's two calls must continue to throw on failure exactly as today (a timeout naturally propagates as a rejected fetch, causing the existing throw paths to fire).Deliverables
fetch()calls insrc/selfhost/qdrant-vectorize.tspasssignal: AbortSignal.timeout(QDRANT_FETCH_TIMEOUT_MS)(or equivalent named constant).queryasserting a hung fetch resolves to{ matches: [] }within the bounded timeout.upsert(ordeleteByIds) asserting a hung fetch rejects with a bounded-time error rather than hanging.Test Coverage Requirements
src/selfhost/qdrant-vectorize.tsis undersrc/**, measured by Codecov. This PR must hit 99%+ patch coverage on every changed line and branch, including the timeout paths exercised by the regression tests above.Expected Outcome
Every Qdrant REST call in
src/selfhost/qdrant-vectorize.tsis bounded by an explicit timeout, consistent with every other external-HTTP self-host adapter in this codebase, so an unresponsive or network-partitioned Qdrant instance can no longer hang the self-host review/RAG request-handling process.Links & Resources
src/selfhost/qdrant-vectorize.ts(initQdrantCollection, lines 66-89;createQdrantVectorize, lines 92-163)src/selfhost/ai.ts(AbortSignal.timeout(120_000))