Conversation
…terator A header-only bgzipped VCF produces a valid tabix index with an empty sequence dictionary. The iterator builds its chromosome map purely from the index contigs, so that map is empty, and both hasNext() and next() advanced hgSeekIndex without a hasNext() guard. The first call threw NoSuchElementException, surfacing as an opaque GorSystemException with a null message rather than an empty result. seek() was already guarded, which is why "gor -p chr1 <file>" returned zero rows cleanly on the same file while an unpositioned "gor <file>" threw. Guard the iterator in hasNext() and return false once the contig set is exhausted, and flip the tail recursion over chromosomes to a loop. next() now delegates to hasNext(), which removes the second unguarded call site and also fixes a latent createRow(null) NPE when the current chromosome's iterator drained without a preceding hasNext(). Add regression tests covering a header-only bgzipped VCF built at test time: hasNext() is false, next() is null, the header still parses, and the query runs end to end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dwg
approved these changes
Sep 1, 2026
gmagnu
deleted the
ENGKNOW-3766-gor-empty-header-only-vcf-crashes-vcf-gz-tabix-genomic-iterator-with-no-such-element-exception
branch
September 1, 2026 23:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A header-only bgzipped VCF (valid header, zero data rows) produces a valid tabix index with an empty sequence dictionary. Reading one throws instead of returning zero rows:
init()builds the chromosome map purely fromreader.getChromosomes()— the contigs listed in the.tbiindex, never from the VCF body — then setshgSeekIndex = chrs.keySet().iterator(). With an empty index that map is empty, and bothhasNext()andnext()calledhgSeekIndex.next()with no guard, so the very firsthasNext()threw before any row was read.seek()was already guarded (while (hgSeekIndex.hasNext())), which is whygor -p chr1 <file>returned zero rows cleanly on the same file while an unpositionedgor <file>threw. That asymmetry is a quick confirmation test.Introduced in bad1981 (2022-08-16), present through 36bd22a.
Repro
Expected zero rows; got the exception above.
Fix
hasNext()guardshgSeekIndex.hasNext()before advancing and returnsfalseonce the contig set is exhausted. The tail recursion over chromosomes becomes awhileloop while we're in there — same behavior, no stack growth on many-contig files.next()delegates tohasNext()and returnsnullwhen exhausted. This removes the second unguarded call site and also fixes a latentcreateRow(null)NPE when the current chromosome's iterator drained without a precedinghasNext().Returning
nullon exhaustion preserves the existing contract and matches the siblingGorGzGenomicIterator. That iterator was checked for the same pattern and has no unguarded call.A
try/catch (NoSuchElementException)was considered instead. Rejected: the guard costs one field-null-check per chromosome rollover (not per row), while catching would also swallow aNoSuchElementExceptionraised for a genuine reason deeper in htsjdk, turning a real bug into a silent empty result.Tests
Four regression tests in
UTestVcfTabixGenomicIterator. The header-only.vcf.gzand its index are built at test time withBlockCompressedOutputStream+IndexFactory.createTabixIndexinto theTemporaryFolderrule's work dir, so no test-data submodule change is needed.hasNext()isfalsenext()isnullgor <file> | top 5runs end to end and yields header-only outputVerified as a proper red/green cycle: with the production fix stashed the three behavioral tests fail with the exact stack from the report; restored, all pass. The header test passes either way, confirming header parsing was never at fault.
./gradlew :model:test— 1514 pass, 17 skipped./gradlew :gortools:testfor the tabix/VCF consumers (UTestGorTabix,UTestGorVcfWithHtsjdk,UTestNor,UTestProcessSource) — 73 pass, 13 skippedImpact
Any header-only VCF — for example a caller that legitimately emits no calls for a sample — surfaced as an opaque system exception with a null message instead of an empty result, giving no indication that the file is simply empty and sending triage toward the storage layer, which was not at fault.
🤖 Generated with Claude Code