The setter stores its argument and then calls a method on it without checking for null; every other place in the class that touches the processor checks first.
StandardQueryParseris built onQueryParserHelper, which parses a query string in three phases: a syntax parser turns the text into a tree of query nodes, a processor rewrites the tree, and a builder turns the tree into aQuery.- The middle phase is optional. For
StandardQueryParserit is a pipeline of 21 processors, among them the analyzer (Foobecomesfoo) and the default operator. setQueryNodeProcessorreplaces that processor. Its javadoc saysnullmeans no processing, andparsethen skips the phase.
null, expected f:Foo against the NPE, the two setter lines at QueryParserHelper.java:101-102, and the null checks the rest of the class has. Select the still to open it at full size. Generated with AI, checked against the code.- Create
new StandardQueryParser(new StandardAnalyzer()):parse("Foo", "f")givesf:foo, because the default processing step runs the analyzer. - Call
setQueryNodeProcessor(null). Its javadoc says "The argument can be null, which means that no processor will be used", so you expectparse("Foo", "f")to skip processing and give the rawf:Foo, as a parser built with anullprocessor in the constructor does. - Instead the call throws
NullPointerException("this.processor" is null) atQueryParserHelper.java:102, with assertions on and off. - Why: line 101 stores the
null, and line 102 callsthis.processor.setQueryConfigHandler(...)on it. The constructor,setQueryConfigHandlerandparseall check fornullfirst; this setter does not. - No query comes out wrong: the field is already
nullwhen it throws, so only the exception is wrong. Workaround: build aQueryParserHelperwith anullprocessor in its constructor (StandardQueryParserhas no constructor that takes one), or catch the NPE.
Code that turns query processing off on an existing parser through this public setter, for example a custom QueryParserHelper that wants raw syntax trees. Every such call fails in the setter itself. Opt-in and rare: the stock parsers never pass null. No query comes out wrong, and code that builds a QueryParserHelper itself can pass null to its constructor.
Mechanism, terms and numbers453 words
The flexible query parser runs three phases: a syntax parser builds a tree of query nodes, an optional processor rewrites it, and a builder turns it into a Query. QueryParserHelper.setQueryNodeProcessor documents that its argument "can be null, which means that no processor will be used to process the query node tree" (QueryParserHelper.java:92-96 at 645d4fe14a20). The body stores the argument (:101) and then calls this.processor.setQueryConfigHandler(getQueryConfigHandler()) without a null check (:102), so the documented call always throws NullPointerException. The constructor (:71), setQueryConfigHandler (:189) and parse (:218) all skip a null processor. StandardQueryParser and PrecedenceQueryParser inherit the setter. The defect dates from the commit that created the class (LUCENE-1567, 2009) and is in every release from 2.9.0 to 10.6.0 and on main. No query comes out wrong: the field is already null when it throws, and code that builds a QueryParserHelper itself can pass null to its constructor instead.
- Three phases, one optional.
parseruns the syntax parser (:214), then the processor onlyif (processor != null)(:216-220), then the builder (:222). The package doc: "at least 2 phases, text parsing and query building, and one optional phase called query processing" (package-info.java:25-26). - Every other entry point handles "no processor". The constructor calls
processor.setQueryConfigHandler(queryConfigHandler)onlyif (processor != null)(:61-74, guard:71);setQueryConfigHandlerdoes the same (:185-192, guard:189); the getter documents anullresult "if no processor is used" (:76-87). - The setter forgets the guard (
:100-103), right under the javadoc that allowsnull(:89-99):
public void setQueryNodeProcessor(QueryNodeProcessor processor) {
this.processor = processor; // :101
this.processor.setQueryConfigHandler(getQueryConfigHandler()); // :102 NPE when processor == null
}- The state is already right when it throws. Line 101 runs first, so afterwards
getQueryNodeProcessor()isnullandparse("Foo", "f")givesf:Foo, exactly the documented no-processor parser. Only the exception is wrong. - Who inherits it.
StandardQueryParser extends QueryParserHelper(StandardQueryParser.java:231) andPrecedenceQueryParser extends StandardQueryParser(PrecedenceQueryParser.java:35); neither overrides the setter. The only callers in the Lucene tree are the twoPrecedenceQueryParserconstructors, which pass a new non-null pipeline (:41,:50); no test calls the setter (tests do assume the default pipeline:TestQPHelper.java:165castsgetQueryNodeProcessor()toQueryNodeProcessorPipeline).
| parser | parse("Foo", "f") |
|---|---|
new StandardQueryParser(new StandardAnalyzer()), default 21-processor pipeline | f:foo (analysed) |
QueryParserHelper built with a null processor in the constructor | f:Foo (no processing) |
the default parser after setQueryNodeProcessor(null) | the call throws NullPointerException; if caught, getQueryNodeProcessor() is null and parse gives f:Foo |
| the same with the null-guard fix | the call returns; f:Foo |
Exposure and likelihood251 words
- Who is hit. Applications that switch the processing phase off on an existing parser through
setQueryNodeProcessor(null): a customQueryParserHelperthat builds the raw syntax tree, or code that swaps processors and clears one. It fails every time, deterministically, in the setter itself, whether or not the parser has parsed queries before. - Default vs opt-in. Opt-in and rare.
StandardQueryParserandPrecedenceQueryParsernever passnull. ForStandardQueryParser, turning processing off switches off all 21 processors of its pipeline: analysis and the default operator, and also wildcard, regexp, fuzzy, range, point, multi-field, phrase-slop, boost, rewrite-method and interval handling and the tree clean-ups;StandardQueryTreeBuildersays it "should be used" on a tree processed by that pipeline (StandardQueryTreeBuilder.java:45-48). So it is an unusual setup, not a general raw-parser mode, and the runs here cover only the single-term queryFoo. GitHub searches of Elasticsearch, OpenSearch and Solr found no use ofsetQueryNodeProcessor. - Effect. A loud
NullPointerException: no wrong query, no index or data effect, constant cost. The harm is a documented call that cannot be used, and a confusing NPE for the user who followed the javadoc. - Workaround. Code that builds a
QueryParserHelperitself (or a subclass that callssuper(...)) can passnullas the processor to its four-argument constructor.StandardQueryParserandPrecedenceQueryParserhave no constructor that takes a processor, so their users buildnew QueryParserHelper(new StandardQueryConfigHandler(), new StandardSyntaxParser(), null, new StandardQueryTreeBuilder())instead (executed:parse("Foo", "f")=f:Foo). Or catch the NPE (the field is already set).
One JUnit test in lucene/queryparser, public API only: a StandardQueryParser, setQueryNodeProcessor(null), then parse("Foo", "f"). Deterministic, single thread. It fails on main with assertions on and off, and on the 9.12.3, 10.5.2 and 10.6.0 release jars.
/*
* 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.flexible.standard;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestSetNullQueryNodeProcessor extends LuceneTestCase {
public void testSetNullProcessor() throws Exception {
StandardQueryParser parser = new StandardQueryParser(new StandardAnalyzer());
try {
parser.setQueryNodeProcessor(null); // javadoc: "The argument can be null"
} catch (NullPointerException e) {
throw new AssertionError("expected null to switch off query processing, got " + e, e);
}
assertEquals(new TermQuery(new Term("f", "Foo")), parser.parse("Foo", "f"));
}
}
cp $REPRO/TestSetNullQueryNodeProcessor.java $LUCENE/lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests org.apache.lucene.queryparser.flexible.standard.TestSetNullQueryNodeProcessor -Ptests.seed=DEADBEEF
# also with -Ptests.asserts=false
- Expected
setQueryNodeProcessor(null)returns, andparse("Foo", "f")gives the unprocessedTermQueryf:Foo.- Observed
NullPointerException("this.processor" is null) atQueryParserHelper.java:102, with assertions on and off.
More files, environment, full output166 words
The try/catch only turns the NPE into an AssertionError that states expected vs actual; without it the test shows the bare NPE. Nothing random reaches the code path; the seed only satisfies the test framework.
Suggested regression test: this same test, as its own class or as one method in TestQPHelper.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); also the 9.12.3, 10.5.2 and 10.6.0 release jars |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux aarch64 (the only platform run; pure Java logic) |
# main 645d4fe14a20, gradle, seed DEADBEEF, -Ptests.asserts=false (the same with true)
TestSetNullQueryNodeProcessor > testSetNullProcessor FAILED
java.lang.AssertionError: expected null to switch off query processing, got java.lang.NullPointerException: Cannot invoke "org.apache.lucene.queryparser.flexible.core.processors.QueryNodeProcessor.setQueryConfigHandler(org.apache.lucene.queryparser.flexible.core.config.QueryConfigHandler)" because "this.processor" is null
at __randomizedtesting.SeedInfo.seed([DEADBEEF:3B1758AF574D57D9]:0)
at org.apache.lucene.queryparser.flexible.standard.TestSetNullQueryNodeProcessor.testSetNullProcessor(TestSetNullQueryNodeProcessor.java:31)
Caused by:
java.lang.NullPointerException: Cannot invoke "org.apache.lucene.queryparser.flexible.core.processors.QueryNodeProcessor.setQueryConfigHandler(org.apache.lucene.queryparser.flexible.core.config.QueryConfigHandler)" because "this.processor" is null
at org.apache.lucene.queryparser.flexible.core.QueryParserHelper.setQueryNodeProcessor(QueryParserHelper.java:102)
at org.apache.lucene.queryparser.flexible.standard.TestSetNullQueryNodeProcessor.testSetNullProcessor(TestSetNullQueryNodeProcessor.java:29)
:lucene:queryparser:test (FAILURE): 1 test, 1 failure
# the same NPE at QueryParserHelper.java:102 with the 9.12.3, 10.5.2 and 10.6.0 release jars (-da)
# with the null-guard fix: 1 test, 0 failures (assertions on and off)- The setter's javadoc and
@paramboth saynullis allowed and means no processing; the getter documentsnullas "no processor is used". - The constructor,
setQueryConfigHandlerandparseall skip anullprocessor; the package doc calls processing optional. - Where the class refuses
null(setSyntaxParser,setQueryBuilder), it says "cannot be null" and throwsIllegalArgumentException, not an NPE.
The full argument246 words
- The contract allows
nulltwice and says what it means: "The argument can benull, which means that no processor will be used to process the query node tree" (QueryParserHelper.java:92-93) and "@param processor ... this argument can benull" (:95-96). The getter: "it returnsnullif no processor is used" (:77-78). - The package design makes processing optional: "one optional phase called query processing" (
package-info.java:25-26); "Second (optional) Phase: Query Processing" (:36); "This phase is optional" (:41). - The class handles
nulleverywhere else: constructor (:71),setQueryConfigHandler(:189),parse(:218). - Null handling was chosen setter by setter:
setSyntaxParserandsetQueryBuilderdocument "cannot benull" (:106-107,:123-124) and throwIllegalArgumentException(:115-117,:132-134);setQueryConfigHandlerdocuments "It can benull" (:177-178) and accepts it. - Tests and history: no test passes
nullto the setter or asserts the NPE, and no CHANGES or MIGRATE entry names it. The javadoc, the unguarded call and the guarded constructor were written in the same commit,343992fcbb4(LUCENE-1567, 2009); later commits never changed the setter's code or the wording of its javadoc: one renamedgetTextParsertogetSyntaxParser(565a6d22b5f, LUCENE-1792, 2009-08-19, before 2.9.0); the others moved the file, edited its license header and javadoc HTML, or reformatted it (last: LUCENE-9570,2cbf261032d, 2021, which only reflowed the setter). No discussion (#2641, #2856, #14168) accepts the NPE.
The devil's-advocate check127 words
Strongest case (from the intended-behaviour checker): the javadoc is stale on an expert hook; no caller or test has passed null in 17 years and nobody reported it; Lucene often treats null arguments as caller errors; the constructor is a workaround (for code that builds the helper itself); so the docs, not the code, should change.
Why it fails: the permission is repeated in the @param, matched by the getter's documented state and the package's "optional phase", and implemented by the class's three other code paths. Where the authors wanted to refuse null, they said so and threw IllegalArgumentException. A doc-only change would leave the constructor, the getter, parse and the package doc saying the opposite. The cross-model reviewer rated the intended argument weak.
Recommended Guard the call, as the constructor does
Add the null check the constructor already has: if (processor != null) processor.setQueryConfigHandler(getQueryConfigHandler()); (+3/-1 lines, one file). With it the minimal test passes with assertions on and off, and the whole lucene/queryparser suite runs clean (534 tests, 0 failures).
public void setQueryNodeProcessor(QueryNodeProcessor processor) {
this.processor = processor;
if (processor != null) {
processor.setQueryConfigHandler(getQueryConfigHandler());
}
}All options and trade-offs2 options · 300 words
The recommended fix was applied with git apply to a scratch worktree of 645d4fe14a20 and run through gradle; the alternative was not run.
In setQueryNodeProcessor (QueryParserHelper.java:100-103), call setQueryConfigHandler only when the new processor is not null.
public void setQueryNodeProcessor(QueryNodeProcessor processor) {
this.processor = processor;
if (processor != null) {
processor.setQueryConfigHandler(getQueryConfigHandler());
}
}Trade-offs
None found. The setter then does what its javadoc says and matches the constructor (:71-73) and setQueryConfigHandler (:189-191); a non-null argument is configured as before in single-threaded use (the fix calls the method on the parameter, which line 101 has just stored in the field; the class has no synchronisation either way).
What the checks validated
- Gradle, scratch worktree of
645d4fe14a20with the repro test added, seed DEADBEEF: the repro test passes with-Ptests.asserts=false; the whole:lucene:queryparser:test(31 classes, 534 tests including the repro) passes with assertions on: 0 failures, 1 skipped. The same suite without the fix: 1 failure, the repro. :lucene:queryparser:checkGoogleJavaFormatpasses with the fix.- Standalone runner with the fixed class shadowed: the repro passes with
-eaand-da. - The hunt's 150-test flexible-parser subset passes stock and with this guard and the hunt's two other fix sketches (in
QueryNodeImplandQueryNodeProcessorPipeline) applied together; the 534-test module run is the one with this guard alone.
Rewrite the setter's javadoc and @param and throw IllegalArgumentException, like setSyntaxParser and setQueryBuilder (:113-120, :130-137). Not run.
Trade-offs
Replaces the NPE with a clear message, but removes a documented mode and leaves the class inconsistent: the constructor still accepts null, the getter still documents it, parse still skips it, and the package doc still calls processing optional.
Downstream products are affected only if their own setup code passes null to this setter; query text never reaches it.
Code reading: while sweeping the flexible parser's processor pipeline and how the configuration handler reaches processors, the finder saw that setQueryNodeProcessor is the only path in QueryParserHelper without a null check. A public-API JUnit then showed the NPE on main and on release jars.
The method in full88 words
Method: non-formal (code reading, then a directed JUnit), 2026-10-28, main a9779ef3c193. The hunt unit's lead was the list views of QueryNodeProcessorPipeline; QueryParserHelper was one of its files. Reading the class showed that the constructor, setQueryConfigHandler and parse guard a null processor and the setter does not. The finder's JUnit (the setter through StandardQueryParser, plus a constructor control) failed 1 of 2 on main (-ea and -da), branch_10_6, 10.5.2 and 9.12.3. A later minimisation step replaced it with the one-method test shown here.
No earlier report found in GitHub issues and PRs (including the JIRA archive), commits, open PRs, the mailing lists, downstream projects or the web (searched 2026-10-28, re-checked 2026-10-30). The only upstream text naming the setter is #14168, which passes a non-null pipeline.
Searches3 searches
Context, not reports: #14168 (open feature request; the only upstream text that names the setter, with a non-null pipeline); #2641 / LUCENE-1567 (introduced the class; no null-processor discussion); #6554 (LUCENE-5491) and #6938 (LUCENE-5876), other NPEs in the flexible parser with other fix sites. The Lucene.NET port has the same unguarded body; that is code, not a report. Limits: keyword search only; private channels are not searchable.
| Where | When | How | Found |
|---|---|---|---|
| GitHub, mailing lists, JIRA archive, web (search script) | 2026-10-28 | 51 queries: 16 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list (dev@, java-user@, issues@, commits@), 4 JIRA archive, 6 web; plus the finder's earlier 45-query run | 166 distinct hits, none about this bug |
| GitHub (apache/lucene, all of GitHub, Elasticsearch, OpenSearch, Solr, Lucene.NET) | 2026-10-28 | 23 manual searches (setQueryNodeProcessor, QueryParserHelper with null or NullPointerException, "this.processor"); the file lists of all 414 open apache/lucene PRs | only #14168 names the setter; no open PR touches QueryParserHelper.java |
| GitHub refresh (apache/lucene) | 2026-10-30 | issues and PRs matching setQueryNodeProcessor, QueryParserHelper and "this.processor" is null; open PRs matching QueryParserHelper; the file lists of the 32 open PRs updated since 2026-10-28; commits touching QueryParserHelper.java | only #14168 and older unrelated issues; no open PR touches the flexible parser; the newest commit on the file is the 2021 reformat |
Born with the class: LUCENE-1567 (343992fcbb4, 2009-08-03) wrote the javadoc and the unguarded call together. First release 2.9.0; every release to 10.6.0 and main is affected; not fixed upstream.
Versions tested8 rows
343992fcbb4 (Subversion r800191, "LUCENE-1567: New flexible QueryParser framework.", committed by Michael Busch; the 2.9 contrib CHANGES credit Luis Alves and Adriano Campos) created QueryParserHelper with the null-allowing javadoc, the unguarded setter and the guarded constructor. It was a JIRA patch (migrated as #2641), so there is no PR. git log -S on the unguarded line finds only this commit on the trunk line; later commits never changed the setter's code or the wording of its javadoc: one renamed getTextParser to getSyntaxParser (565a6d22b5f, LUCENE-1792, 2009-08-19, before 2.9.0); the others moved the file, edited its license header and javadoc HTML, or reformatted it (last: LUCENE-9570, 2cbf261032d, 2021, which only reflowed the setter). A bytecode scan of all 147 final lucene-queryparser releases from 2.9.0 to 10.5.2 finds no null test before the call. Upstream main 2f424fd428da and branch_10_6 (2026-10-30) still have the unguarded line.
| Version or commit | Verdict | Result |
|---|---|---|
| 2.4.1 | API absent | no flexible parser (compile error; no lucene-queryparser artifact) |
| 2.9.0, 3.6.2, 4.0.0, 8.11.4, 9.0.0 | first release 2.9.0 | NPE in setQueryNodeProcessor (portable main() repro); the constructor control passes |
| 9.1.0, 10.0.0 | affected | NPE at QueryParserHelper.java:102 (the finder's JUnit) |
9.12.3, 10.5.2, 10.6.0 (4ae946e5a9b9) | affected | the same NPE with the minimal test (Maven Central jars, -da) |
| all 147 final releases 2.9.0 to 10.5.2 | bytecode scan | no null test before the call; setQueryConfigHandler has one (control, from the fact check's re-scan) |
main 645d4fe14a20 | affected | the same NPE, gradle and standalone, assertions on and off |
upstream main 2f424fd428da, branch_10_6 | source read | lines 100-103 unchanged (2026-10-30) |
main 645d4fe14a20 + null guard | fix | the minimal test passes; the whole queryparser suite: 534 tests, 0 failures, 1 skipped |
Independent verifiers, a reading-only cross-model review and an adjudication confirmed the finder's report, and a credibility audit kept it; the review's refute lens said not real only because it could not run code. This doc's runs reproduced it and validated the fix through gradle.
Checks and verdicts279 words
Every executed check reproduced the same NPE at QueryParserHelper.java:102, and every constructor control passed.
| Check | Verdict | What it found |
|---|---|---|
| finder (code reading, then a JUnit) | candidate | Fails 1 of 2 on main (-ea, -da), branch_10_6, 10.5.2 and 9.12.3; the guard passes 2 of 2; 150 of 150 flexible-parser tests pass stock and with the guard plus the hunt's two other fix sketches. |
| Independent reproducer | real | Own 4-test JUnit: the setter through StandardQueryParser and through a plain QueryParserHelper fail on main (-ea, -da), 10.5.2 and 9.12.3; controls pass; 4 of 4 with the guard. |
| Cross-model review (OpenAI GPT-5.6, reading only) | intended lens: real, medium; refute lens: not real (procedural) | The refute lens could not run code and calls that a tooling failure, not contrary evidence; it agrees on the root cause. Neither lens ran code. The hunt's own follow-up JUnit (not run by the reviewer) fails 1 of 3 on main and passes 3 of 3 with the guard. |
| Intended-behaviour devil's advocate | real, minor | Built the strongest not-a-bug case and showed why it fails; its survey shows the no-processor state after the NPE and the same NPE through PrecedenceQueryParser. |
| adjudication | confirmed, high | Re-ran the independent reproduction's JUnit (-ea, -da: 2 of 4 fail at QueryParserHelper.java:102) and the finder's. |
| credibility audit | keep | Own main() repro: the NPE through StandardQueryParser and PrecedenceQueryParser; the constructor control parses. |
| this doc's runs (2026-10-30) | reproduced; fix validated | The minimal test fails on main 645d4fe14a20 (gradle and standalone, -ea and -da) and on 9.12.3, 10.5.2 and 10.6.0; with the guard it passes, and the whole queryparser suite has 0 failures in 534 tests. |
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 469 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,098 words
- Registry title
- Flexible query parser: QueryParserHelper.setQueryNodeProcessor(null) throws NullPointerException, although its javadoc says null is allowed and means no query-node processing (StandardQueryParser and PrecedenceQueryParser inherit the setter; every release since 2.9.0)
- Area
- queryparser / flexible core (QueryParserHelper; inherited by StandardQueryParser and PrecedenceQueryParser)
- Priority
- Low, rank 660: It is a loud NPE at parser setup when setQueryNodeProcessor(null) is called, a rare expert configuration that the javadoc allows and that has a constructor workaround, the same profile as the javadoc-allows-null NPE LFV-606.
- Impact class
- crash
- Impact note
- Hits only applications that turn off query-tree processing through this public setter (a rare, expert configuration: no in-tree caller or test passes null); the failure is loud and immediate at parser setup, there is no wrong result, the field is already null when the exception is thrown, and passing a null processor to the QueryParserHelper constructor is a working alternative.
- Summary
- You call setQueryNodeProcessor(null) on a StandardQueryParser, a PrecedenceQueryParser or any QueryParserHelper to switch off the optional query-processing phase, as the method's javadoc allows ('The argument can be null, which means that no processor will be used to process the query node tree'). You expect the parser to go on parsing without processing, as it does when the null processor is passed to the QueryParserHelper constructor. Instead the setter always throws NullPointerException, because it stores the argument and then calls setQueryConfigHandler on it without the null check that the constructor, setQueryConfigHandler and parse all have.
- Introduced
- 343992fcbb4b31249f07354014723f18d0508d8a (Subversion lucene/java/trunk r800191, 'LUCENE-1567: New flexible QueryParser framework.', the commit that created QueryParserHelper; JIRA LUCENE-1567, migrated to GitHub as #2641; there is no pull request). The null-allowing javadoc and the unguarded dereference were written together in that commit, next to the null-guarded constructor, so the defect was born with the API and is not a regression [read: git blame -C -C -M and git log -S 'this.processor.setQueryConfigHandler(getQueryConfigHandler());' in the full-history clone; later commits on the file only moved, relicensed or reformatted it, the last being 2cbf261032d, LUCENE-9570, 2021] (2009-08-03 (author and commit date of 343992fcbb4))
- First affected version
- 2.9.0 [exec] (the first release with the flexible query parser; 2.4.1 has no such API: compile_error [exec], and no lucene-queryparser artifact exists for it)
- Affected versions
- every final lucene-queryparser release from 2.9.0 to 10.5.2 (147 versions [read]: bytecode scan of SHA-1-verified Maven Central jars shows no null test before the call in setQueryNodeProcessor, with setQueryConfigHandler's null test as the positive control; 4.0.0-ALPHA/BETA not checked), plus 10.6.0 (branch_10_6 4ae946e5a9b) and main. Executed [exec]: 2.9.0, 3.6.2, 4.0.0, 8.11.4, 9.0.0, 9.12.3, 10.5.2 and main with a main() variant (-ea); 9.1.0, 9.12.3, 10.0.0, 10.5.2 and main with the JUnit (-ea); 9.12.3, 10.5.2, branch_10_6 4ae946e5a9b and main with the JUnit (-da). Every run fails with the same NullPointerException in setQueryNodeProcessor, and the constructor control passes.
- 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 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 (the prior-report record, verdict new, searched 2026-10-28, read-only): a scripted prior-report search with 51 queries (16 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list over dev@, java-user@, issues@ and commits@, 4 JIRA-archive, 6 web; 166 distinct hits, 0 high) plus the finder's earlier 45-query run (0 high); 23 manual GitHub searches (apache/lucene, all of GitHub, Elasticsearch, OpenSearch, Solr, Lucene.NET); all 414 open apache/lucene PRs checked by file list, none touches QueryParserHelper.java. The only upstream text that names the setter is #14168 (open feature request; it passes a non-null pipeline); the NPE-titled flexible-parser issues #6554 (LUCENE-5491) and #6938 (LUCENE-5876) have other fix sites. re-checked (2026-10-28): 'setQueryNodeProcessor' still finds only #14168; no issue or PR mentioning QueryParserHelper created since 2026-10-20; none of the 72 PRs updated since 2026-10-26 touches QueryParserHelper.java; the file's newest upstream commit is still 2cbf261032d (LUCENE-9570 reformat, author date 2021-01-05).
- Lucene commit tested
- main
645d4fe14a20(11.0.0-SNAPSHOT), 2026-10-30: the minimal test through gradle in a scratch worktree with assertions on and off, stock and with the recommended fix (plus the wholelucene/queryparsertest suite with the fix and without it), and on the standalone runner with-eaand-da, stock and with the fixed class shadowed; the 9.12.3, 10.5.2 and 10.6.0 release jars (-da). The hunt's history runs cover 2.9.0 to 10.5.2 (2.9.0, 3.6.2, 4.0.0, 8.11.4, 9.0.0, 9.1.0, 9.12.3, 10.0.0, 10.5.2 executed) and a bytecode scan of all 147 final releases from 2.9.0 to 10.5.2. The hunt itself ran on maina9779ef3c193, whereQueryParserHelper.javais identical. Upstream main2f424fd428daandbranch_10_6were read on 2026-10-30. Only Linux aarch64 and OpenJDK 25.0.3 were run.
- Finding record
lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/QueryParserHelper.java:61-74 (at a9779ef3c193) (constructor: stores the processor, null guard at :71 before processor.setQueryConfigHandler), :76-87 (getQueryNodeProcessor javadoc: 'it returns <code>null</code> if no processor is used'), :89-99 (setQueryNodeProcessor javadoc: 'The argument can be <code>null</code>, which means that no processor will be used to process the query node tree'; @param: 'this argument can be <code>null</code>'), :100-103 (setter body: :101 assigns this.processor, :102 calls this.processor.setQueryConfigHandler(getQueryConfigHandler()) unconditionally), :185-192 (setQueryConfigHandler: null guard at :189), :213-222 (parse: skips a null processor at :218)lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/package-info.java:25-26 (at a9779ef3c193) ('at least 2 phases, text parsing and query building, and one optional phase called query processing') and :36-42 ('Second (optional) Phase: Query Processing' ... 'This phase is optional and is used only if an extra processing, validation, query expansion, etc needs to be performed')lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/precedence/PrecedenceQueryParser.java:41 (at a9779ef3c193) and :50 (the only non-test callers of the setter; both pass a new non-null PrecedenceQueryNodeProcessorPipeline)
- The finding in 30 seconds
lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/QueryParserHelper.java:61-74, :76-87, :89-103, :185-192, :213-223lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/package-info.java:25-26, :36-42lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:231, :235-242lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/precedence/PrecedenceQueryParser.java:35, :41, :50lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/StandardQueryNodeProcessorPipeline.java:45-70 (21 processors; AnalyzerQueryNodeProcessor at :58)grep -rn setQueryNodeProcessor lucene (only PrecedenceQueryParser.java:41, :50 call it)lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/TestQPHelper.java:165 (casts getQueryNodeProcessor() to QueryNodeProcessorPipeline)
- Who is affected
lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/precedence/PrecedenceQueryParser.java:41, :50lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/StandardQueryNodeProcessorPipeline.java:45-70lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/builders/StandardQueryTreeBuilder.java:45-48 ("should be used" with a tree processed by StandardQueryNodeProcessorPipeline)
- Why it looks unintended
lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/QueryParserHelper.java:76-87, :89-103, :105-137, :176-192, :213-223lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/package-info.java:25-26, :36-42lucene/CHANGES.txt and lucene/MIGRATE.md (no mention of setQueryNodeProcessor or QueryParserHelper)grep -rn setQueryNodeProcessor lucene (no test calls it)
- Suggested fix
lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/QueryParserHelper.java:71-73, :100-103, :113-120, :130-137, :189-191
- Diagrams
lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/QueryParserHelper.java:71, :92-93, :100-103, :189, :218
- TestSetNullQueryNodeProcessor.java (lucene/queryparser test, one method):
repro/LFV-968/src/org/apache/lucene/queryparser/flexible/standard/TestSetNullQueryNodeProcessor.java(35 lines)
assets/LFV-968/bug.png(469 KB)
