One dead check: the builder's own "missing occurs means SHOULD" branch tests for null, but a missing XML attribute reads as the empty string.
CoreParser.parse(InputStream)turns an XML document into a LuceneQuery: a<BooleanQuery>element becomes aBooleanQuery, and each<Clause>child becomes one of its clauses.- A clause's
occursattribute picks its role:should(optional),must(required),mustNot(excluded) orfilter. - The shipped
LuceneCoreQuery.dtddocuments this syntax and makesoccursoptional with the defaultshould. The parser does not validate against the DTD, so the builder has to apply that default itself.
<Clause> without occurs: getAttribute returns the empty string, the null check meant for this case misses it, and the parse ends at the throw instead of SHOULD. Select the still to open it at full size. Generated with AI, checked against the code.- Parse
<BooleanQuery fieldName='f'><Clause><TermQuery>a</TermQuery></Clause></BooleanQuery>withnew CoreParser("f", analyzer).parse(...). The<Clause>has nooccursattribute. - You expect a
BooleanQuerywith one optional (SHOULD) clausef:a: the DTD givesoccursthe default"should", the same document withoccurs='should'parses to exactly that, and 5.0.0 treats a missingoccursas SHOULD. - Instead the whole parse fails with
ParserException: Invalid value for "occurs" attribute of clause:, with nothing after the colon. An explicitoccurs=""fails the same way. - Why:
getOccursValuereadsgetAttribute("occurs"), which returns""for a missing attribute, nevernull. Itsoccs == nullbranch, meant to return SHOULD, never runs, so""reaches the throw. - It fails closed: no query is built and the parser stays usable;
occurs="should"works around it. Every release from 5.1.0 to 10.6.0 rejects the clause; 5.0.0 accepts it.
Users of the opt-in XML query parser who leave occurs out of a <Clause>, as the DTD allows, or write occurs="". The whole query is refused with a checked ParserException: no wrong results, no setting involved. Published examples write occurs out, so hits are rare; occurs="should" is the workaround.
Mechanism, terms and numbers586 words
In the XML query parser (CoreParser, CorePlusQueriesParser, CorePlusExtensionsParser), the occurs attribute of a <Clause> says whether the clause is optional (should), required (must), excluded (mustNot) or a filter. The shipped LuceneCoreQuery.dtd declares <!ATTLIST Clause occurs (should | must | mustnot) "should">, its generated docs say "Default value: should", and BooleanQueryBuilder.getOccursValue has its own branch for a missing attribute. Yet CoreParser.parse of <BooleanQuery fieldName='f'><Clause><TermQuery>a</TermQuery></Clause></BooleanQuery> throws ParserException: Invalid value for "occurs" attribute of clause:. org.w3c.dom.Element.getAttribute returns "" for an absent attribute, so the occs == null branch is dead and "" falls through to the throw; occurs="" fails the same way. LUCENE-6270 introduced it in 5.1.0 while adding occurs="filter": the rewrite replaced the original "".equals(occs) test with occs == null. The parse fails cleanly, with no wrong results; also treating an empty value as SHOULD fixes it.
- XML query parser:
org.apache.lucene.queryparser.xmlinlucene-queryparser.CoreParserregisters aBooleanQueryBuilderfor<BooleanQuery>(CoreParser.java:93at645d4fe14a20);CorePlusQueriesParserandCorePlusExtensionsParserinherit it. Element.getAttribute: the DOM call the builders use. Its JDK javadoc: "TheAttrvalue as a string, or the empty string if that attribute does not have a specified or default value."- DTD default:
<!ATTLIST Clause occurs (should | must | mustnot) "should">(LuceneCoreQuery.dtd:124). An XML parser fills such a default in only when it reads the DTD;CoreParserparses without validation (CoreParser.java:184) and its default entity resolver refuses external entities (:169-171,:218-226), so a document cannot pull the shipped DTD in.
static BooleanClause.Occur getOccursValue(Element clauseElem) throws ParserException {
String occs = clauseElem.getAttribute("occurs"); // :71, "" when absent
if (occs == null || "should".equalsIgnoreCase(occs)) { // :72, null never happens
return BooleanClause.Occur.SHOULD;
} else if ("must".equalsIgnoreCase(occs)) {
return BooleanClause.Occur.MUST;
} else if ("mustNot".equalsIgnoreCase(occs)) {
return BooleanClause.Occur.MUST_NOT;
} else if ("filter".equals(occs)) {
return BooleanClause.Occur.FILTER;
}
throw new ParserException("Invalid value for \"occurs\" attribute of clause:" + occs); // :81
}BooleanQueryBuilder.getQuerycallsgetOccursValuefor each<Clause>(BooleanQueryBuilder.java:54).- With no
occurs,getAttributereturns""(:71); a DOM built like CoreParser's giveshasAttribute("occurs") == falseandgetAttribute("occurs") == "". occs == null(:72) is false and""matches none of the four names, so:81throws, with the empty value after the colon. The exception leavesCoreParser.parse(CoreParser.java:141-142), which declaresParserException; the caller gets no query.
The same builder reads its other two attributes correctly, through DOMUtils.getAttribute(e, name, default), which treats null or empty as "use the default" (DOMUtils.java:119-137): minimumNumberShouldMatch (:46, default 0) and boost (:63, default 1.0), the defaults the DTD declares (LuceneCoreQuery.dtd:115, :109). Only occurs is read raw and tested for null.
<Clause ...> inside <BooleanQuery fieldName='f'> | result |
|---|---|
occurs='should' | BooleanQuery with SHOULD f:a |
no occurs | ParserException: Invalid value for "occurs" attribute of clause: |
occurs='' | the same exception |
occurs='sometimes' (invalid) | rejected, as it should be |
no occurs, the shipped ATTLIST repeated in an internal DTD subset | SHOULD f:a (the XML parser fills in the default) |
no occurs, with <!DOCTYPE BooleanQuery SYSTEM "LuceneCoreQuery.dtd"> | refused: External Entity resolving unsupported |
no occurs, via CorePlusQueriesParser or CorePlusExtensionsParser | the same ParserException |
| a valid document on the same parser instance afterwards | parses |
In 5.x from 5.1.0 on, <BooleanFilter><Clause> without occurs was rejected the same way (run on 5.1.0 and 5.5.5); that builder is gone since 6.0.0.
Exposure and likelihood170 words
- Who is hit. Applications that use
CoreParser,CorePlusQueriesParserorCorePlusExtensionsParser(public, in the exported packagesorg.apache.lucene.queryparser.xmland.builders) and send a<Clause>withoutoccurs, or withoccurs="": hand-written queries, query templates (the DTD names XSLT query templates as a motivation) or generators that rely on the documented default. Solr'sxmlparserquery parser reaches the same builder (code reading, not run). - Default vs opt-in. The XML query parser is opt-in; inside it no setting is needed, the default
CoreParser(String, Analyzer)constructor is enough. Assertions on or off make no difference. - What happens. The whole query fails with a checked
ParserException; nothing is built or searched, so there are no wrong results, and the same parser instance keeps working. - Scale. None: one clause triggers it.
- Likelihood: rare. All 28
<Clause>elements in the upstream test fixtures and the DTD's own example writeoccursout; a hit fails loudly during development; no public report of it since 5.1.0 (2015) was found. - Workaround. Write
occurs="should"on every optional clause.
One JUnit test on the public API: parse a <BooleanQuery> with one <Clause> that has no occurs, and compare the result with a BooleanQuery holding SHOULD f:a. Deterministic, single thread, no index.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.queryparser.xml;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestClauseDefaultOccurs extends LuceneTestCase {
public void testClauseWithoutOccursIsShould() throws Exception {
String xml =
"<BooleanQuery fieldName='f'><Clause><TermQuery>a</TermQuery></Clause></BooleanQuery>";
Query expected =
new BooleanQuery.Builder().add(new TermQuery(new Term("f", "a")), Occur.SHOULD).build();
try (Analyzer analyzer = new StandardAnalyzer()) {
Query actual;
try {
actual =
new CoreParser("f", analyzer)
.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
} catch (ParserException e) {
throw new AssertionError("expected one SHOULD clause f:a (default occurs), got " + e, e);
}
assertEquals(expected, actual);
}
}
}
cp TestClauseDefaultOccurs.java $LUCENE/lucene/queryparser/src/test/org/apache/lucene/queryparser/xml/
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests org.apache.lucene.queryparser.xml.TestClauseDefaultOccurs -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
- Expected
- A
<Clause>withoutoccursparses to aBooleanQuerywith one SHOULD clausef:a. - Observed
ParserException: Invalid value for "occurs" attribute of clause:fromBooleanQueryBuilder.getOccursValue(line 81), with assertions on and off.
More files, environment, full output185 words
Pure XML parsing: no index and no randomness (TermQuery does not analyze; the seed does not matter). The ParserException is rethrown as an AssertionError that states expected and actual, with the original exception as its cause. It compiles against 9.1 and later (the org.apache.lucene.tests.util package); older releases were checked with a plain main() probe.
As the regression test: the same test, for example as a new method in TestCoreParser.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); the same test fails on 10.6.0 and 10.5.2, and the hunt's earlier version on 9.12.3 |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (pure XML parsing; no platform dependence expected) |
cp $REPRO/TestClauseDefaultOccurs.java $LUCENE/lucene/queryparser/src/test/org/apache/lucene/queryparser/xml/
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests org.apache.lucene.queryparser.xml.TestClauseDefaultOccurs -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false
$ ./gradlew :lucene:queryparser:test --tests org.apache.lucene.queryparser.xml.TestClauseDefaultOccurs -Ptests.seed=DEADBEEF
TestClauseDefaultOccurs > testClauseWithoutOccursIsShould FAILED
java.lang.AssertionError: expected one SHOULD clause f:a (default occurs), got org.apache.lucene.queryparser.xml.ParserException: Invalid value for "occurs" attribute of clause:
at __randomizedtesting.SeedInfo.seed([DEADBEEF:C1CC01360CA0A832]:0)
at org.apache.lucene.queryparser.xml.TestClauseDefaultOccurs.testClauseWithoutOccursIsShould(TestClauseDefaultOccurs.java:44)
Caused by:
org.apache.lucene.queryparser.xml.ParserException: Invalid value for "occurs" attribute of clause:
at app//org.apache.lucene.queryparser.xml.builders.BooleanQueryBuilder.getOccursValue(BooleanQueryBuilder.java:81)
at app//org.apache.lucene.queryparser.xml.builders.BooleanQueryBuilder.getQuery(BooleanQueryBuilder.java:54)
at app//org.apache.lucene.queryparser.xml.QueryBuilderFactory.getQuery(QueryBuilderFactory.java:34)
at app//org.apache.lucene.queryparser.xml.CoreParser.getQuery(CoreParser.java:210)
at app//org.apache.lucene.queryparser.xml.CoreParser.parse(CoreParser.java:142)
at app//org.apache.lucene.queryparser.xml.TestClauseDefaultOccurs.testClauseWithoutOccursIsShould(TestClauseDefaultOccurs.java:42)
BUILD FAILED in 1m 8s- The builder's own code maps a missing
occursto SHOULD (occs == null, line 72), but that branch can never run. - The DTD declares
occurswith the default"should"; nothing documents it as required. - 5.0.0 and earlier parse it as SHOULD; the 5.1 rewrite that added
filterdropped the empty-string test, with no CHANGES note.
The full argument286 words
- The builder's own default.
BooleanQueryBuilder.java:72returns SHOULD foroccs == null. That branch maps an attribute read as missing to SHOULD, and with the JDK DOM it cannot run. - The rewrite kept the old null case and lost the empty case. Before LUCENE-6270 the method started from SHOULD, mapped
"should"or""to SHOULD (("should".equalsIgnoreCase(occs)) || ("".equals(occs))) and threw only for an unknown value that was not null (if (occs != null)).bed131f1808(2015-02-21) rewrote the method to addoccurs="filter", one part of LUCENE-6270's replacement of TermsFilter with TermsQuery, and turned that intooccs == null || "should".equalsIgnoreCase(occs). - Documentation.
LuceneCoreQuery.dtd:123-124: "Controls if the clause is optional (should), mandatory (must) or unacceptable (mustNot)" and<!ATTLIST Clause occurs (should | must | mustnot) "should">; the DTD says it "describes the XML syntax" and that "CoreParser.java is the Java class that encapsulates this parser behaviour" (:20,:66). The generated docs (lucene/queryparser/docs/xml/LuceneCoreQuery.dtd.html:136,:142) say "Default value: should". No javadoc,CHANGES.txtorMIGRATE.mdentry makesoccursrequired. - History. From the parser's first commit (2006; the 1.9 release has the identical file) a missing or empty
occursmeant SHOULD; 4.10.4 and 5.0.0 parse the hunt's probe tof:a +f:b. The LUCENE-6270 CHANGES entry (lucene/CHANGES.txt:11095-11096) says only "Replaced TermsFilter with TermsQuery, use a QueryWrapperFilter(TermsQuery) instead."; the 5.1.0 MIGRATE.txt and the issue's 12 comments never mentionoccurs. No upstream test asserts the rejection. - Precedent. LUCENE-7219 (#8274, 6.0.1 "Bug Fixes",
lucene/CHANGES.txt:9296-9298) made the range builders'lowerTerm/upperTermoptional, matching the underlying queries: they had been read withgetAttributeOrFail, and the fix reads them throughDOMUtils.getAttribute(e, name, null), which treats""as absent.
The devil's-advocate check192 words
Strongest case for intended (the reviewers rated it moderate): CoreParser parses without validation and refuses to load the DTD, so DTD defaults have no runtime force; the DTD is stale elsewhere (it documents FilteredQuery, RangeFilter, NumericRangeQuery, BooleanFilter and disableCoord, which core no longer builds, and omits occurs="filter"); every upstream test, stylesheet and DTD example writes occurs out; and the failure is loud. So occurs is effectively required and the DTD is documentation debt.
Why it fails: the non-validating parser is exactly why the builder must apply the default itself, and it tries to: the occs == null branch reads as "missing means SHOULD", and the same builder applies the DTD's defaults for boost and minimumNumberShouldMatch. The stale DTD entries describe removed elements; Clause and occurs are live. A deliberate tightening in a minor release would have come with a CHANGES or MIGRATE note and a "missing attribute" message, not Invalid value ... : with an empty value. Fixtures that always write occurs pin nothing about the absent case. In our reading the argument limits severity, not validity; this is evidence about intent, not proof of it.
Recommended A. Also treat an empty value as SHOULD (one line)
Treat an empty occurs like a missing one: if (occs == null || occs.isEmpty() || "should".equalsIgnoreCase(occs)). With it the repro passes with assertions on and off, the whole lucene/queryparser suite passes (534 tests, 0 failures) and check is clean. Reading the attribute through DOMUtils.getAttribute(e, "occurs", "should") is an equivalent alternative.
static BooleanClause.Occur getOccursValue(Element clauseElem) throws ParserException {
String occs = clauseElem.getAttribute("occurs");
- if (occs == null || "should".equalsIgnoreCase(occs)) {
+ if (occs == null || occs.isEmpty() || "should".equalsIgnoreCase(occs)) {
return BooleanClause.Occur.SHOULD;All options and trade-offs2 options · 473 words
Both options were run through gradle in a scratch worktree of 645d4fe14a20 and restore the pre-5.1 behaviour: a missing or empty occurs means SHOULD; explicit values, invalid values and the error message are unchanged.
- Not recommended: declare
occursrequired (DTD#REQUIRED, regenerated docs, a "missingoccurs" message). It would document today's behaviour but contradicts the builder's own default, the docs since 2007 and the 1.9-5.0 behaviour. Not run. - A design choice, not run: to keep
occurs=""an error (it is not one of the DTD's values), test!clauseElem.hasAttribute("occurs")instead ofisEmpty(); that differs from pre-5.1 behaviour and from howDOMUtilstreats empty attributes. - CHANGES entry: say plainly that a missing
occursmeans SHOULD (optional), as the DTD documents, not MUST or FILTER, so nobody who builds restriction clauses from templates reads the restored default as restrictive.
In BooleanQueryBuilder.getOccursValue (BooleanQueryBuilder.java:72), add occs.isEmpty() to the SHOULD test.
static BooleanClause.Occur getOccursValue(Element clauseElem) throws ParserException {
String occs = clauseElem.getAttribute("occurs");
- if (occs == null || "should".equalsIgnoreCase(occs)) {
+ if (occs == null || occs.isEmpty() || "should".equalsIgnoreCase(occs)) {
return BooleanClause.Occur.SHOULD;Trade-offs
- Restores the 1.9-5.0 behaviour for a missing or empty
occursand matches the DTD default;occurs="filter", added in 5.1, keeps working. - Keeps the
nullguard, which is dead with the JDK DOM; option B drops it. - An explicit
occurs=""becomes SHOULD, as before 5.1 and asDOMUtilstreats empty attributes.
What the checks validated
Gradle, scratch worktree of 645d4fe14a20 (2026-11-01): the repro fails on the stock tree and passes with A, with assertions on and off; the whole :lucene:queryparser:test task passes with A and the repro (534 tests in 31 classes, 0 failures, 1 skipped); :lucene:queryparser:check -x test passes. Earlier, with A shadowed over the main jars, the three upstream XML parser test classes pass 69 of 69, as without it.
Read the attribute through DOMUtils.getAttribute(clauseElem, "occurs", "should"), which maps null or "" to the default, and drop the dead null test.
static BooleanClause.Occur getOccursValue(Element clauseElem) throws ParserException {
- String occs = clauseElem.getAttribute("occurs");
- if (occs == null || "should".equalsIgnoreCase(occs)) {
+ // a missing (or empty) occurs attribute means SHOULD, the default in LuceneCoreQuery.dtd
+ String occs = DOMUtils.getAttribute(clauseElem, "occurs", "should");
+ if ("should".equalsIgnoreCase(occs)) {
return BooleanClause.Occur.SHOULD;Trade-offs
- Same behaviour as A, written the way
getQueryalready readsminimumNumberShouldMatchandboostand the way LUCENE-7219 fixed the range builders. - States the default in one place;
DOMUtilsis already imported.
What the checks validated
Gradle, same worktree: the repro passes with B, with assertions on and off; the org.apache.lucene.queryparser.xml test classes pass (70 tests incl. the repro, 0 failures); :lucene:queryparser:check -x test passes. The whole module was not run for B.
Solr's xmlparser query parser reaches the same builder (code reading, not run), so Solr XML queries that leave out occurs would fail the same way.
Non-formal: comparing the XML query builders with the syntax LuceneCoreQuery.dtd documents showed the dead occs == null branch. A public-API JUnit test reproduced the rejection on main, and release jars placed the change between 5.0.0 and 5.1.0.
The method in full92 words
Method: non-formal (code reading, then executed tests). On 2026-10-26 the hunt unit for the flexible, simple and XML query parsers set up, as its secondary method, a comparison of each XML builder's output with the syntax LuceneCoreQuery.dtd documents. Reading getOccursValue against the DTD's occurs default showed the null test that the DOM never satisfies. A public-API JUnit test reproduced the rejection on main, a main() probe on release jars found the boundary (5.0.0 accepts, 5.1.0 rejects), independent checkers reproduced it with their own tests, and it was cut to the one-test repro.
No prior report found (GitHub issues, PRs and commits incl. the JIRA archive, open PRs, mailing lists, downstream projects, the web; 2026-10-26 and 2026-11-01). Closest: #9788, open since 2019, the same DOM pitfall in DOMUtils.getAttributeOrFail; a sibling with a different fix.
Searches4 searches
Closest items, none a report of this bug:
- #9788 (LUCENE-8743, "DOMUtils.getAttributeOrFail(Element,String) never fails", open since 2019, by Christine Poerschke, patch attached, never committed): the same DOM fact in a different method, with the opposite symptom (a missing required attribute is accepted). Its patch changes only
getAttributeOrFail;getOccursValuedoes not callDOMUtils, so it would leave this bug as it is (code reading, not executed). They are separate defects. - #9787 (LUCENE-8742): its companion; added the test that still expects a
NumberFormatExceptionfor a missing SpanNearslop. - #7332 (LUCENE-6270): the introducing change; none of its comments mentions
occurs. - #15053 "Cleanup DOMUtils" (open, stale): removes dead null checks in
DOMUtilsonly. - #8274 (LUCENE-7219): the same class of bug in the range builders, fixed in 2016 (precedent).
- SOLR-11477 (2017,
926cc4d65b6;lucene/CHANGES.txt:7955-7956, 7.1.0): madeCoreParserrefuse external entities by default (CoreParser.java:169-171,:218-226); it is why a document cannot pull in the shipped DTD and its default. - #9338 (LUCENE-8291, 7.4): a report about external entities in the XML query parser module; it ended with the removal of
QueryTemplateManagerand the demo webapp and did not changeCoreParser. Unrelated to this bug. - #8091 (LUCENE-7034), SOLR-839 (Solr
defType=xmlparser), #4420, #5085: none contains a<Clause>withoutoccurs, and none reports the rejection.
Limits: GitHub search is phrase-like; the list archives' full-text search has limited coverage; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues and PRs (incl. the migrated JIRA issues), commits | 2026-10-26 and 2026-11-01 | scripted prior-report searches (35, 86 and 88 queries) plus manual searches: class, method and attribute names, the error text, LUCENE-6270, DOMUtils | no report; #9788 (sibling), #7332 (introducing change), #15053, #8274 |
| Open PRs | 2026-10-26 and 2026-11-01 | changed-file lists of all 410 open apache/lucene PRs | none touches BooleanQueryBuilder.java or the DTD |
| JIRA (LUCENE, SOLR) and mailing lists | 2026-10-26 and 2026-11-01 | Lucene dev@, java-user@, issues@, commits@, general@; Solr users@, dev@, issues@, old solr-user@; full reads of LUCENE-8743, LUCENE-8742 and SOLR-839 | only commit mails carrying the diff; LUCENE-8743 (the sibling) |
| Downstream projects and the web | 2026-10-26 and 2026-11-01 | Elasticsearch, OpenSearch, Solr, lucene-solr, Lucene.NET; web search; StackOverflow | nothing about this defect; Lucene.NET's 4.8-based port still maps "" to SHOULD |
Introduced in 5.1.0 by LUCENE-6270 (bed131f1808, backport 21fd3aa83d5, 2015-02-21), which replaced the original "".equals(occs) test with occs == null. 5.0.0 accepts the clause; every release from 5.1.0 to 10.6.0 and main reject it.
Versions tested6 rows
Introduced by LUCENE-6270 "Replace TermsFilter with TermsQuery" (#7332, by Adrien Grand, @jpountz; a JIRA patch, no pull request): trunk commit bed131f1808 (svn r1661395, first trunk-line release 6.0.0) and the same-day branch_5x backport 21fd3aa83d5 (svn r1661412), which shipped first, in 5.1.0. It is the only commit between the 5.0.0 and 5.1.0 tags that touches the file. Before it, from the parser's first commit 87768c51c63 (Mark Harwood, 2006) a missing or empty occurs meant SHOULD; the DTD default came in 2007 (21a07ee41e2). All 49 release tags 1.9 to 5.0.0 have the old test and all 98 tags 5.1.0 to 10.6.0 the new one. The newest change to the file is the 2021 reformat 2cbf261032d; upstream main 2f424fd428da (2026-10-30) still has the bug.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal repro fails with assertions on and off (through Gradle and standalone); passes with fix A or B |
| 10.6.0, 10.5.2 | affected | minimal repro fails with assertions on and off (release jars) |
| 9.12.3 | affected | the hunt's JUnit fails |
| 5.1.0, 6.0.0, 7.7.3, 8.11.4 | first affected 5.1.0 | main() probe: rejected with the same ParserException |
5.1.0, 5.5.5 (<BooleanFilter>) | affected | a <BooleanFilter><Clause> without occurs is rejected too (that builder is gone since 6.0.0) |
| 4.10.4, 5.0.0 | last unaffected 5.0.0 | main() probe parses to f:a +f:b |
Confirmed, high confidence: the finder, an independent reproducer, an intended-behaviour devil's advocate, an adjudication and a tie-breaker reproduced it on main with assertions on and off. Reviewers from another model family, unable to run code, agreed with the root cause.
Checks and verdicts263 words
Everyone agrees on the root cause and that it is a loud wrong error on input the DTD allows, never a wrong query.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | A two-assertion JUnit fails on main (assertions on and off), 10.5.2 and 9.12.3; the fix sketch passes it and 45 of 45 upstream XML parser tests. |
| cross-model review, refute lens (OpenAI GPT-5.6, read-only), twice | not real, low (procedural) | Could not run code, so its verdict is procedural; it agreed with the root cause and rated the intended case moderate. Separate re-runs fail. |
| independent reproducer | real | Its own JUnit fails on main (assertions on and off), 10.5.2 and 9.12.3; 3 controls pass; its probe: 5.0.0 accepts, 5.1.0 rejects. |
| cross-model review, intended lens (OpenAI GPT-5.6, read-only) | real, medium | The works-as-designed case is moderate but fails: nothing documents occurs as required. |
| intended-behaviour devil's advocate | real, low impact | Showed that a DOCTYPE pointing at the shipped DTD is refused, so the builder's default is the only practical way the documented default can apply. |
| adjudication | confirmed, high | Re-ran the reproducer's JUnit with assertions on and off (fails) and its controls (pass). |
| re-check on 645d4fe14a20 (reproducer and tie-breaker) | real, high | Main, 10.5.2 and 10.6.0 fail with assertions on and off; 8 characterisation tests; 69 of 69 upstream XML parser tests with and without fix A; the 5.0.0/5.1.0 boundary re-run. |
| Registration check, audit and this doc | confirmed | Re-runs fail on main; the one-test repro fails on 645d4fe14a20 through gradle and passes with fix A or B. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 446 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,111 words
- Registry title
- XML query parser: a <Clause> without the occurs attribute, which LuceneCoreQuery.dtd and BooleanQueryBuilder's own code default to SHOULD, is rejected with ParserException 'Invalid value for "occurs" attribute of clause:' (regression since 5.1.0, LUCENE-6270)
- Area
- queryparser / xml (BooleanQueryBuilder)
- Priority
- Low, rank 1069: The niche XML query parser loudly rejects a clause that leaves out occurs (the DTD default) and the workaround is obvious.
- Impact class
- wrong error
- Impact note
- Applications that use the opt-in XML query parser (CoreParser and its subclasses CorePlusQueriesParser and CorePlusExtensionsParser, with default settings; downstream wrappers such as Solr's xmlparser query parser reach the same code [read]) and send a <Clause> without occurs, or with occurs="", relying on the documented SHOULD default, get a ParserException for the whole query on every such input; it fails closed (no Query is built, no wrong results, the parser stays usable), needs no scale, is in every release since 5.1.0, and writing occurs="should" avoids it.
- Summary
- You give the XML query parser (CoreParser.parse) a <BooleanQuery> whose <Clause> has no occurs attribute and expect an optional (SHOULD) clause, because LuceneCoreQuery.dtd declares occurs with the default "should", BooleanQueryBuilder.getOccursValue has its own 'attribute absent -> SHOULD' branch, and 5.0.0 and earlier parsed it that way. Instead the whole parse fails with ParserException 'Invalid value for "occurs" attribute of clause:' (nothing after the colon): org.w3c.dom.Element.getAttribute returns an empty string, never null, for an absent attribute, so the 'occs == null' default branch never runs; an explicit occurs="" is rejected the same way. Explicit should, must, mustNot and filter values work as before, and writing occurs="should" is a workaround.
- Introduced
- bed131f1808d40a4f7f79303f73e1c93aa750895 (trunk, svn r1661395) and its branch_5x backport 21fd3aa83d581add2a7a69a2def32b5b8983e4a9 (svn r1661412, the change that first shipped, in 5.1.0), LUCENE-6270 'Replace TermsFilter with TermsQuery' (migrated as GitHub #7332; by jpountz; JIRA-patch era, no PR) [read: git blame of BooleanQueryBuilder.java:72-81 and git log -S'occs == null' in the history clone; exec: 5.0.0 accepts, 5.1.0 rejects]. The rewrite of getOccursValue, done to add occurs="filter", replaced the original '"".equals(occs)' -> SHOULD test (87768c51c63, 2006, first released in 1.9) with 'occs == null', which can never be true because Element.getAttribute returns "" for an absent attribute (2015-02-21)
- First affected version
- 5.1.0 [exec] (5.0.0 is the last unaffected release [exec]); on the trunk line 6.0.0 [exec]
- Affected versions
- every release from 5.1.0 through 10.6.0, and main ea57360912ed. Executed: main() probe rejects on 5.1.0, 6.0.0, 7.7.3, 8.11.4, 9.12.3, 10.5.2 and main (-ea); the JUnit fails on 9.12.3, 10.5.2 and main with -ea and -da [exec]. All 97 release tags 5.1.0-10.5.2 in the history clone contain 'occs == null ||' (8.8.2-8.11.4 are not tagged there; 8.11.4 executed) [read]. Not affected: 1.9 through 5.0.0 (all 49 tags have the old '"".equals(occs)' -> SHOULD form [read]; 4.10.4 and 5.0.0 executed, both parse the test query to 'f:a +f:b'). Upstream main unchanged on 2026-10-26 (last change to the file 2cbf261032d, a 2021 reformat) [read]. In 5.1-5.5.x BooleanFilterBuilder (CorePlusExtensionsParser) also called getOccursValue, so <BooleanFilter><Clause> without occurs is rejected too [exec 2026-11-01: a main() probe rejects it on 5.1.0 and 5.5.5 and accepts it on 5.0.0; that builder is gone since 6.0.0]. The 10.6.0 release jars fail the one-test minimal repro with -ea and -da [exec 2026-11-01]. All runs: OpenJDK 25.0.3, Linux aarch64
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an adjudication that ruled it real, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- non-formal: code reading
- Upstream and prior reports
- none found (2026-10-26, 227 read-only query records: two scripted prior-report searches (35 and 86 queries over GitHub issues and PRs, commits, downstream repositories, the mailing lists, the JIRA archive and the web) and 106 manual queries: GitHub issue, PR and commit search in apache/lucene and downstream (Elasticsearch, OpenSearch, Solr, lucene-solr, Lucene.NET), the file lists of all 410 open apache/lucene PRs (none touches BooleanQueryBuilder), LUCENE and SOLR JQL, the Lucene dev@, java-user@, issues@, commits@ and general@ lists and the Solr users@, dev@, issues@ and old solr-user@ lists, the web, and local git history and CHANGES). Closest items, none the same bug: #7332 (LUCENE-6270, the introducing change; none of its 12 comments mentions occurs); open stale PR #15053 (removes the same kind of dead null check, but only in DOMUtils); #8274 (LUCENE-7219, the same bug class fixed in the range builders in 2016); open #8091 (LUCENE-7034, XML builder docs request). Not fixed upstream; no open PR touches the file. Gaps: GitHub search is phrase-like; a private report to the security list would not be visible
- Lucene commit tested
- main
645d4fe14a20(the minimal repro with assertions on and off, through gradle and standalone; fixes A and B through gradle in a scratch worktree); the 10.6.0 and 10.5.2 release jars (assertions on and off); the hunt's JUnit on 9.12.3; amain()probe on 4.10.4 and 5.0.0 (accept) and 5.1.0, 6.0.0, 7.7.3 and 8.11.4 (reject). The hunt ran on mainea57360912ed, whoselucene/queryparsermodule is identical. Upstream main2f424fd428da(2026-10-30) has the same file (read).
- Finding record
lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/builders/BooleanQueryBuilder.java:54 (at ea57360912ed) (getQuery calls getOccursValue), :70-81 (getOccursValue: Element.getAttribute at :71, dead 'occs == null' at :72, throw at :81)lucene/queryparser/src/resources/org/apache/lucene/queryparser/xml/LuceneCoreQuery.dtd:123-124 (at ea57360912ed) ('Controls if the clause is optional (should) ...'; <!ATTLIST Clause occurs (should | must | mustnot) "should">)lucene/queryparser/docs/xml/LuceneCoreQuery.dtd.html:142 (at ea57360912ed) (Clause@occurs: 'Possible values: should, must, mustnot - Default value: should')lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/CoreParser.java:141-142 (at ea57360912ed) (parse(InputStream))lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/DOMUtils.java:47-53 (at ea57360912ed) (getAttributeOrFail, same dead null check; side observation only)
- The finding in 30 seconds
lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/builders/BooleanQueryBuilder.java:44-82 (at 645d4fe14a20)lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/CoreParser.java:93, 141-142, 169-171, 182-206, 218-226 (at 645d4fe14a20)lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/DOMUtils.java:119-137 (at 645d4fe14a20)lucene/queryparser/src/resources/org/apache/lucene/queryparser/xml/LuceneCoreQuery.dtd:109, 115, 123-124 (at 645d4fe14a20)JDK 25 java.xml sources: org/w3c/dom/Element.java (getAttribute javadoc)
- Who is affected
lucene/queryparser/src/java/module-info.java:50-51 (at 645d4fe14a20)lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/CoreParser.java:69-93, 141-142 (at 645d4fe14a20)lucene/queryparser/src/resources/org/apache/lucene/queryparser/xml/LuceneCoreQuery.dtd:20-30, 95-103 (at 645d4fe14a20)grep of lucene/queryparser/src/test at 645d4fe14a20: 28 <Clause> elements, all with occurs (8 should, 13 must, 5 mustnot, 2 filter)
- Why it looks unintended
lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/builders/BooleanQueryBuilder.java:46, 54, 63, 70-82 (at 645d4fe14a20)lucene/queryparser/src/resources/org/apache/lucene/queryparser/xml/LuceneCoreQuery.dtd:20, 66, 109, 115, 123-124 (at 645d4fe14a20)lucene/queryparser/docs/xml/LuceneCoreQuery.dtd.html:136, 142 (at 645d4fe14a20)lucene/CHANGES.txt:11095-11096 (5.1.0, LUCENE-6270) and 9296-9298 (6.0.1, LUCENE-7219) (at 645d4fe14a20)
- Suggested fix
lucene/CHANGES.txt:9296-9298 (LUCENE-7219) (at 645d4fe14a20)
- TestClauseDefaultOccurs.java (JUnit, one test):
repro/LFV-361/src/org/apache/lucene/queryparser/xml/TestClauseDefaultOccurs.java(49 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the repro's input and output.
assets/LFV-361/bug.png(446 KB)
