efetch db=biosample ignores the accession prefix¶
Archive: NCBI E-utilities
Affected source: biosample:attribute
Status: detected, mechanism confirmed, worked around; not yet reported upstream
Measured: 2026-09-16
The symptom¶
efetch db=biosample appears to accept BioSample accessions. Asked for
SAMEA11294529 -- a human liver sample in E-MTAB-10553 -- it returns
SAMN11294529, a rumen bacterium, with HTTP 200, a well-formed
BioSampleSet, and 14 attributes.
requested SAMEA11294529 -> SAMN11294529, "Tissierellaceae bacterium", 14 attributes
requested SAMEA11294527 -> SAMN11294527, "Pseudoselenomonas ruminantium", 14 attributes
Nothing in the response says a substitution happened. A count check passes: one sample was asked for and one was returned.
The reproducer¶
# Asked for an EBI-registered human sample; answered with an NCBI bacterium.
curl -s "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=biosample&id=SAMEA11294529&retmode=xml" \
| grep -o 'accession="[^"]*"\|<OrganismName>[^<]*'
The mechanism¶
The alphabetic prefix is stripped and the remainder is read as a UID.
# These three return the same record.
for id in SAMEA11294529 11294529 SAMN11294529; do
curl -s "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=biosample&id=$id&retmode=xml" \
| grep -o 'accession="[^"]*"' | head -1
done
# -> accession="SAMN11294529" (all three)
11294529 is the Entrez UID of SAMN11294529. The real SAMEA11294529 is UID
23423599, which esearch finds correctly:
curl -s "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=biosample&term=SAMEA11294529&retmode=json"
# -> "idlist": ["23423599"]
Why it is invisible on most samples¶
For an NCBI-registered accession the number is the UID.
SAMN12011228 -> uid 12011228
SAMN12039425 -> uid 12039425
So id=SAMN12011228 returns SAMN12011228 -- not because the accession was
honoured, but because stripping the prefix happens to yield the right UID. Every
SAMN sample works by coincidence, and SAMN is most of them.
The bug bites only where the registration authority is not NCBI: SAMEA
(EBI) and SAMD (DDBJ). This project's corpus is full of those -- ENA finds
125,271 BioSamples NCBI's own link table does not, 93.6% of them in
EBI-registered projects.
What it is not¶
Not a missing record. SAMEA11294529 is present in NCBI BioSample and
esearch returns exactly one hit for it. Only the efetch id parameter is
affected.
Not a malformed-request error. There is no 400, no <ERROR> element and no
warning. The substituted record is complete and internally consistent, which is
what makes it dangerous: a metadata layer reading it attributes a rumen
bacterium's host, isolation_source and env_medium to a human liver sample.
The workaround¶
Resolve every accession through esearch ... [accession] first, then efetch
by UID, then check every returned accession against what was asked for.
uids = await biosample_uids(client, batch, timeout) # esearch, [accession] field
xml = await efetch_text(client, db="biosample", uids=uids, retmode="xml", ...)
for record in parse_biosample_set(xml):
if record.accession in expected: # the check that catches the substitution
kept.append(record)
Searching on the [accession] field rather than as a bare term matters for the
same reason: a bare number can match a UID or an unrelated field. The term goes
by POST above 200 accessions, because 200 of them is over 5 KB and
a UID list in a query string has cost this project data five times.
Implemented in eutils.fetch_biosamples. It costs one extra request per batch
of 200, which is the price of not silently mixing organisms.
Regression test¶
tests/pathologies/test_biosample_accession_substitution.py, inverted like the
rest of the suite: it asserts the substitution still happens, and separately
that id=SAMEA11294529 and id=11294529 still return the same record, so the
mechanism is pinned rather than only the symptom. A third test, not inverted,
asserts the workaround still defeats the live bug.
A failure of the first two is good news. Drop the esearch hop, update this
page, and delete them.