All findings · Lucene finding · LFV-361 · xml-clause-missing-occurs-rejected

XML query parser rejects a <Clause> without occurs instead of treating it as SHOULD

The XML query parser's DTD says a <Clause> without occurs is optional (should), but since 5.1.0 CoreParser.parse rejects it with ParserException: Invalid value for "occurs" attribute of clause:. The code checks the attribute for null, while the XML DOM returns "" for a missing attribute.

Summary

Priority
Low, rank 1069The niche XML query parser loudly rejects a clause that leaves out occurs (the DTD default) and the workaround is obvious.
Impact
wrong error
Since
5.1.0 (LUCENE-6270, bed131f1808 and backport 21fd3aa83d5, 2015-02-21)introduced 2015-02-21 · over 11 years
Affected
5.1.0-10.6.0 and main
Status
documented
Found by
non-formal: reading the XML builders against their DTD, then a public-API JUnit test

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

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.

Background: how the XML query parser builds a BooleanQuery

  1. CoreParser.parse(InputStream) turns an XML document into a Lucene Query: a <BooleanQuery> element becomes a BooleanQuery, and each <Clause> child becomes one of its clauses.
  2. A clause's occurs attribute picks its role: should (optional), must (required), mustNot (excluded) or filter.
  3. The shipped LuceneCoreQuery.dtd documents this syntax and makes occurs optional with the default should. The parser does not validate against the DTD, so the builder has to apply that default itself.
Diagram of the bug: the XML <BooleanQuery fieldName='f'><Clause><TermQuery>a</TermQuery></Clause></BooleanQuery> goes to CoreParser.parse. BooleanQueryBuilder.getOccursValue reads getAttribute("occurs"), which returns the empty string for the missing attribute. The check occs == null, meant to return SHOULD, is false, no other value matches, and the parser throws ParserException: Invalid value for "occurs" attribute of clause:. Expected: a BooleanQuery with one SHOULD clause f:a, as the DTD default "should" and Lucene 5.0.0 give.
A <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.

The finding, step by step

  1. Parse <BooleanQuery fieldName='f'><Clause><TermQuery>a</TermQuery></Clause></BooleanQuery> with new CoreParser("f", analyzer).parse(...). The <Clause> has no occurs attribute.
  2. You expect a BooleanQuery with one optional (SHOULD) clause f:a: the DTD gives occurs the default "should", the same document with occurs='should' parses to exactly that, and 5.0.0 treats a missing occurs as SHOULD.
  3. Instead the whole parse fails with ParserException: Invalid value for "occurs" attribute of clause:, with nothing after the colon. An explicit occurs="" fails the same way.
  4. Why: getOccursValue reads getAttribute("occurs"), which returns "" for a missing attribute, never null. Its occs == null branch, meant to return SHOULD, never runs, so "" reaches the throw.
  5. 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.

Who is affected

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.

Terms

  • XML query parser: org.apache.lucene.queryparser.xml in lucene-queryparser. CoreParser registers a BooleanQueryBuilder for <BooleanQuery> (CoreParser.java:93 at 645d4fe14a20); CorePlusQueriesParser and CorePlusExtensionsParser inherit it.
  • Element.getAttribute: the DOM call the builders use. Its JDK javadoc: "The Attr value 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; CoreParser parses 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.

The bug

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
}
  1. BooleanQueryBuilder.getQuery calls getOccursValue for each <Clause> (BooleanQueryBuilder.java:54).
  2. With no occurs, getAttribute returns "" (:71); a DOM built like CoreParser's gives hasAttribute("occurs") == false and getAttribute("occurs") == "".
  3. occs == null (:72) is false and "" matches none of the four names, so :81 throws, with the empty value after the colon. The exception leaves CoreParser.parse (CoreParser.java:141-142), which declares ParserException; 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.

What happens with each input (main, assertions on and off)

<Clause ...> inside <BooleanQuery fieldName='f'>result
occurs='should'BooleanQuery with SHOULD f:a
no occursParserException: 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 subsetSHOULD 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 CorePlusExtensionsParserthe same ParserException
a valid document on the same parser instance afterwardsparses

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, CorePlusQueriesParser or CorePlusExtensionsParser (public, in the exported packages org.apache.lucene.queryparser.xml and .builders) and send a <Clause> without occurs, or with occurs="": hand-written queries, query templates (the DTD names XSLT query templates as a motivation) or generators that rely on the documented default. Solr's xmlparser query 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 write occurs out; 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.

Minimal repro

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.

TestClauseDefaultOccurs.java (JUnit, one test)49 lines
/*
 * 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);
    }
  }
}
Command
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> without occurs parses to a BooleanQuery with one SHOULD clause f:a.
Observed
ParserException: Invalid value for "occurs" attribute of clause: from BooleanQueryBuilder.getOccursValue (line 81), with assertions on and off.

All files of this repro: repro/LFV-361/

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.

Environment

lucenemain 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
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64 (pure XML parsing; no platform dependence expected)
Command (environment)
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

Observed output

$ ./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

Why it looks unintended

  • The builder's own code maps a missing occurs to SHOULD (occs == null, line 72), but that branch can never run.
  • The DTD declares occurs with the default "should"; nothing documents it as required.
  • 5.0.0 and earlier parse it as SHOULD; the 5.1 rewrite that added filter dropped the empty-string test, with no CHANGES note.
The full argument286 words
  • The builder's own default. BooleanQueryBuilder.java:72 returns SHOULD for occs == 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 add occurs="filter", one part of LUCENE-6270's replacement of TermsFilter with TermsQuery, and turned that into occs == 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.txt or MIGRATE.md entry makes occurs required.
  • History. From the parser's first commit (2006; the 1.9 release has the identical file) a missing or empty occurs meant SHOULD; 4.10.4 and 5.0.0 parse the hunt's probe to f: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 mention occurs. 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/upperTerm optional, matching the underlying queries: they had been read with getAttributeOrFail, and the fix reads them through DOMUtils.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.

Suggested fix

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 occurs required (DTD #REQUIRED, regenerated docs, a "missing occurs" 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 of isEmpty(); that differs from pre-5.1 behaviour and from how DOMUtils treats empty attributes.
  • CHANGES entry: say plainly that a missing occurs means SHOULD (optional), as the DTD documents, not MUST or FILTER, so nobody who builds restriction clauses from templates reads the restored default as restrictive.

A. Also treat an empty value as SHOULD (one line) (validated by the checks) (recommended)

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 occurs and matches the DTD default; occurs="filter", added in 5.1, keeps working.
  • Keeps the null guard, which is dead with the JDK DOM; option B drops it.
  • An explicit occurs="" becomes SHOULD, as before 5.1 and as DOMUtils treats 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.

B. Read occurs like the builder's other attributes (validated by the checks)

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 getQuery already reads minimumNumberShouldMatch and boost and the way LUCENE-7219 fixed the range builders.
  • States the default in one place; DOMUtils is 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.

Downstream note

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.

How it was found

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.

Prior reports, history and checks

Prior reports

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; getOccursValue does not call DOMUtils, 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 NumberFormatException for a missing SpanNear slop.
  • #7332 (LUCENE-6270): the introducing change; none of its comments mentions occurs.
  • #15053 "Cleanup DOMUtils" (open, stale): removes dead null checks in DOMUtils only.
  • #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): made CoreParser refuse 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 QueryTemplateManager and the demo webapp and did not change CoreParser. Unrelated to this bug.
  • #8091 (LUCENE-7034), SOLR-839 (Solr defType=xmlparser), #4420, #5085: none contains a <Clause> without occurs, 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.

What was searched, when, and what turned up
WhereWhenHowFound
apache/lucene issues and PRs (incl. the migrated JIRA issues), commits2026-10-26 and 2026-11-01scripted prior-report searches (35, 86 and 88 queries) plus manual searches: class, method and attribute names, the error text, LUCENE-6270, DOMUtilsno report; #9788 (sibling), #7332 (introducing change), #15053, #8274
Open PRs2026-10-26 and 2026-11-01changed-file lists of all 410 open apache/lucene PRsnone touches BooleanQueryBuilder.java or the DTD
JIRA (LUCENE, SOLR) and mailing lists2026-10-26 and 2026-11-01Lucene dev@, java-user@, issues@, commits@, general@; Solr users@, dev@, issues@, old solr-user@; full reads of LUCENE-8743, LUCENE-8742 and SOLR-839only commit mails carrying the diff; LUCENE-8743 (the sibling)
Downstream projects and the web2026-10-26 and 2026-11-01Elasticsearch, OpenSearch, Solr, lucene-solr, Lucene.NET; web search; StackOverflownothing about this defect; Lucene.NET's 4.8-based port still maps "" to SHOULD

History

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.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal repro fails with assertions on and off (through Gradle and standalone); passes with fix A or B
10.6.0, 10.5.2 affectedminimal repro fails with assertions on and off (release jars)
9.12.3 affectedthe hunt's JUnit fails
5.1.0, 6.0.0, 7.7.3, 8.11.4 first affected 5.1.0main() probe: rejected with the same ParserException
5.1.0, 5.5.5 (<BooleanFilter>) affecteda <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.0main() probe parses to f:a +f:b

Independent checks

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.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidateA 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 realIts 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, mediumThe works-as-designed case is moderate but fails: nothing documents occurs as required.
intended-behaviour devil's advocate real, low impactShowed 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, highRe-ran the reproducer's JUnit with assertions on and off (fails) and its controls (pass).
re-check on 645d4fe14a20 (reproducer and tie-breaker) real, highMain, 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 confirmedRe-runs fail on main; the one-test repro fails on 645d4fe14a20 through gradle and passes with fix A or B.

Diagram file

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.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,111 words

Finding record

LFV-361, snapshot of 2026-11-06

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; a main() 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 main ea57360912ed, whose lucene/queryparser module is identical. Upstream main 2f424fd428da (2026-10-30) has the same file (read).

Lucene and upstream sources

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)

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Made in parallel with this doc from the code at 645d4fe14a20 and the repro's input and output.

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.