2026-05-28 — NCBI accession + SequenceDetail hardening (10 critique items)¶
Motivation¶
Follow-up to 2026-05-28-ncbi-accession-sequence-detail.md. A 10-item self-critique surfaced four classes of bugs in the just-shipped pipeline:
- Sticky pooled HTTP defaults —
_pooled_clientbakedAcceptinto a shared httpx slot. JSON and bytes calls reuse the same slot, so the second caller would silently inherit the first caller'sAcceptheader. - Half-read responses on stream overflow —
request_bytesraised mid-stream without closing the response, leaving httpx's connection pool in an unknown state on the next NCBI call. - Cache hygiene — TTL cache used shallow
dict(payload)(nested feature lists shared by reference across callers) and FIFO eviction (hot accessions paged out even when freshly accessed). - Validation surface gaps —
query_accession+query_datasilently picked one (audit ambiguity); whole-sequence BLAST > 5 MiB landed as a 503 retryable instead of a 422 user-fixable; the dashboard rendered a broken internal<Link>for non-accession BLAST hits (Query_1, custom DB IDs); URL handoff wiped an in-progress FASTA draft without confirmation.
User-facing change¶
- BlastHitsTable — non-accession
sseqidvalues now render plain text + an external NCBI search link instead of a broken in-app SequenceDetail link. - SequenceDetail — summary → GenBank → FASTA now load sequentially. A typo accession only burns one rate-limit token instead of three, and the page renders incrementally instead of all-or-nothing. Whole-sequence BLAST > 5 Mnt is gated by a
window.confirmso researchers can switch to a sub-range first. - BlastSubmit —
/blast/submit?accession=…handoff preserves an existing inline FASTA draft and surfaces aKept your existing FASTA drafttoast. Submitting both still results in 422 backend-side, but the UI no longer destroys work silently. - Submit error codes — accession resolution now distinguishes
ncbi_query_too_large(422, fix-the-input) fromncbi_lookup_unavailable(503, retryable). Mixed query sources raiseconflicting_query_sources(422) instead of silent precedence. - Sequence Viewer iframe — sandbox flags are unchanged, but the embed now ships with an inline comment explaining why
allow-scripts allow-same-originis required for the NCBI viewer to render and what would have to change to drop it.
Backend diff summary¶
api/services/ncbi/_eutils.py:_pooled_client(slot)now ships onlyUser-Agentheaders. Callers passAcceptper-request.request_json/request_bytespassheaders={"Accept": ...}per call.request_bytesshort-circuits onContent-Lengthand explicitlyresponse.close()s before raising on overflow.- New
NcbiResponseTooLarge(NcbiServiceUnavailable)subclass for non-retryable size-cap overflows. api/services/ncbi/__init__.pyre-exportsNcbiResponseTooLarge.api/services/ncbi/nuccore.py:- Cache buckets switched to
OrderedDictwithmove_to_endon hit (true LRU). _cache_get/_cache_putnowcopy.deepcopypayloads in both directions; callers can mutate freely without polluting the cache.api/services/blast/accession_resolver.pymapsNcbiResponseTooLargetoHTTPException(422, ncbi_query_too_large)with a sub-range hint.api/services/blast/submit_payload.pyrejectsquery_accession+ (query_data|query_file|query_blob_url) withHTTPException(422, conflicting_query_sources)BEFORE resolving the accession.
Frontend diff summary¶
web/src/pages/blastResults/analytics/helpers.ts:- New
ACCESSION_PATTERNmirroring the backend_ACCESSION_RE. - New
isNcbiAccessionLike(accession)predicate gating in-app SequenceDetail links. web/src/pages/blastResults/analytics/BlastHitsTable.tsx:- Subject cell branches on
isNcbiAccessionLike(hit.sseqid). Accession-like sseqids retain the internal<Link>+ external icon; everything else renders plain text + an external NCBI search link. web/src/pages/sequence/SequenceDetail.tsx:genbankQuery.enabledwaits forsummaryQuery.data;fastaQuery.enabledwaits forgenbankQuery.data.launchBlastconfirms before navigating whensummary.length > 5_000_000and!hasHighlight, citing the backend error code in the dialog.- Inline comment on the sviewer
<iframe>documents whyallow-scripts allow-same-originis required and what would have to change to revisit. web/src/pages/BlastSubmit.tsx:- URL handoff useEffect now reads
form.query_datafrom mount state and skips the accession overwrite when an inline FASTA draft is present. Both branches emit distinct toasts.
API / IaC diff¶
- New error code in
/api/blast/jobs/submit: conflicting_query_sources(422) when accession + inline FASTA / file / blob are both supplied.ncbi_query_too_large(422) when the resolved FASTA exceeds the 5 MiB cap; suggestsquery_accession_seq_start/query_accession_seq_stop.- No IaC changes.
Validation evidence¶
uv run pytest -q api/tests/test_ncbi_nuccore.py api/tests/test_blast_submit_accession.py→ 49 passed (pre-edit also 49, but two tests rewritten —test_normalise_query_data_conflicts_with_accession+test_normalise_query_file_conflicts_with_accession).uv run pytest -q api/tests→ 1743 passed, 3 skipped, 2 failed (test_peering_nsg.py, untracked WIP unrelated to this change).cd web && npm test -- --run→ 394 passed (53 files).cd web && npm run build→ success, 9.75s. No new chunk-size warnings beyond the pre-existing ones.uv run ruff check api→ clean.
Self-review summary¶
- Consumer search:
extractCanonicalAccession,internalSequenceRoute,ncbiNuccoreUrl,ncbiSearchUrlconsumers checked inBlastHitsTable.tsx; helpers.test.ts (22 tests) still passes.NcbiServiceUnavailableconsumers checked — onlyaccession_resolver.pypreviously caught it; newNcbiResponseTooLargeis caught before the genericNcbiServiceUnavailablehandler. - Backward-compat: New
NcbiResponseTooLargesubclassesNcbiServiceUnavailableso any existingexcept NcbiServiceUnavailablestill catches both — only the specific resolver short-circuits earlier. - Wide test sweep: backend 1743 / 1745 (2 unrelated WIP failures), frontend 394 / 394, build clean, ruff clean.
- Diff audit: only intended files dirty.
web/src/pages/sequence/SequenceDetail.tsxandweb/src/pages/blastResults/analytics/helpers.tsare new from the prior change note; the additions here are bracketed by existing exports. - Fixture parity: helpers test fixtures only cover the helpers themselves; the changed
isNcbiAccessionLikepredicate is exercised through the BlastHitsTable model test (passes). No backend mocks needed updating because the new 422 codes use the existingHTTPException(detail=dict)pattern.
Items NOT included¶
- The two pre-existing
test_peering_nsg.pyfailures belong to a separate WIP branch (api/tasks/azure/peering_nsg.pyis currently untracked) — out of scope for this change note.