All findings · Lucene finding · LFV-968 · queryparserhelper-setquerynodeprocessor-null-npe

QueryParserHelper.setQueryNodeProcessor(null) throws NullPointerException although its javadoc allows null

setQueryNodeProcessor(null) is documented to switch the flexible query parser's processing step off, but on a StandardQueryParser, a PrecedenceQueryParser or any QueryParserHelper it always throws NullPointerException: it stores the null and then calls a method on it. Every release since 2.9.0 (2009) has it.

Summary

Priority
Low, rank 660It 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
crash
Since
2.9.0 (2009, LUCENE-1567), when the flexible parser was addedintroduced 2009-08-03 · over 17 years
Affected
every release from 2.9.0 to 10.6.0, and main
Status
documented
Found by
code reading of QueryParserHelper, then a public-API JUnit

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

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.

Background: the flexible query parser's three phases

  1. StandardQueryParser is built on QueryParserHelper, 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 a Query.
  2. The middle phase is optional. For StandardQueryParser it is a pipeline of 21 processors, among them the analyzer (Foo becomes foo) and the default operator.
  3. setQueryNodeProcessor replaces that processor. Its javadoc says null means no processing, and parse then skips the phase.
Summary card for QueryParserHelper.setQueryNodeProcessor(null). The three phases of StandardQueryParser: text parsing, processing set to null (off), query building. Expected from the javadoc: the setter returns and parse("Foo", "f") builds the unprocessed TermQuery f:Foo; actual: NullPointerException at QueryParserHelper.java:102, in the setter call, with no wrong results; the default parser gives f:foo for comparison. Why: the javadoc sentence allowing null, then lines 101 and 102 of the setter (store the argument, then call setQueryConfigHandler on it without a null check); the constructor, setQueryConfigHandler and parse all check for null, and the sibling setters for the parser and the builder throw IllegalArgumentException instead. Affected: every final release from 2.9.0 to 10.6.0 and main; workaround: pass null to the QueryParserHelper constructor.
The minimal repro step by step: the three parser phases with processing set to 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.

The finding, step by step

  1. Create new StandardQueryParser(new StandardAnalyzer()): parse("Foo", "f") gives f:foo, because the default processing step runs the analyzer.
  2. Call setQueryNodeProcessor(null). Its javadoc says "The argument can be null, which means that no processor will be used", so you expect parse("Foo", "f") to skip processing and give the raw f:Foo, as a parser built with a null processor in the constructor does.
  3. Instead the call throws NullPointerException ("this.processor" is null) at QueryParserHelper.java:102, with assertions on and off.
  4. Why: line 101 stores the null, and line 102 calls this.processor.setQueryConfigHandler(...) on it. The constructor, setQueryConfigHandler and parse all check for null first; this setter does not.
  5. No query comes out wrong: the field is already null when it throws, so only the exception is wrong. Workaround: build a QueryParserHelper with a null processor in its constructor (StandardQueryParser has no constructor that takes one), or catch the NPE.

Who is affected

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.

The mechanism (main 645d4fe14a20, QueryParserHelper.java)

  1. Three phases, one optional. parse runs the syntax parser (:214), then the processor only if (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).
  2. Every other entry point handles "no processor". The constructor calls processor.setQueryConfigHandler(queryConfigHandler) only if (processor != null) (:61-74, guard :71); setQueryConfigHandler does the same (:185-192, guard :189); the getter documents a null result "if no processor is used" (:76-87).
  3. The setter forgets the guard (:100-103), right under the javadoc that allows null (:89-99):
public void setQueryNodeProcessor(QueryNodeProcessor processor) {
  this.processor = processor;                                   // :101
  this.processor.setQueryConfigHandler(getQueryConfigHandler()); // :102  NPE when processor == null
}
  1. The state is already right when it throws. Line 101 runs first, so afterwards getQueryNodeProcessor() is null and parse("Foo", "f") gives f:Foo, exactly the documented no-processor parser. Only the exception is wrong.
  2. Who inherits it. StandardQueryParser extends QueryParserHelper (StandardQueryParser.java:231) and PrecedenceQueryParser extends StandardQueryParser (PrecedenceQueryParser.java:35); neither overrides the setter. The only callers in the Lucene tree are the two PrecedenceQueryParser constructors, which pass a new non-null pipeline (:41, :50); no test calls the setter (tests do assume the default pipeline: TestQPHelper.java:165 casts getQueryNodeProcessor() to QueryNodeProcessorPipeline).

The numbers (executed on main)

parserparse("Foo", "f")
new StandardQueryParser(new StandardAnalyzer()), default 21-processor pipelinef:foo (analysed)
QueryParserHelper built with a null processor in the constructorf: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 fixthe 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 custom QueryParserHelper that 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. StandardQueryParser and PrecedenceQueryParser never pass null. For StandardQueryParser, 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; StandardQueryTreeBuilder says 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 query Foo. GitHub searches of Elasticsearch, OpenSearch and Solr found no use of setQueryNodeProcessor.
  • 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 QueryParserHelper itself (or a subclass that calls super(...)) can pass null as the processor to its four-argument constructor. StandardQueryParser and PrecedenceQueryParser have no constructor that takes a processor, so their users build new QueryParserHelper(new StandardQueryConfigHandler(), new StandardSyntaxParser(), null, new StandardQueryTreeBuilder()) instead (executed: parse("Foo", "f") = f:Foo). Or catch the NPE (the field is already set).

Minimal repro

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.

TestSetNullQueryNodeProcessor.java (lucene/queryparser test, one method)35 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.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"));
  }
}
Command
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, and parse("Foo", "f") gives the unprocessed TermQuery f:Foo.
Observed
NullPointerException ("this.processor" is null) at QueryParserHelper.java:102, with assertions on and off.

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

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.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); also the 9.12.3, 10.5.2 and 10.6.0 release jars
jdkOpenJDK 25.0.3
osLinux aarch64 (the only platform run; pure Java logic)

Observed output

# 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)

Why it looks unintended

  • The setter's javadoc and @param both say null is allowed and means no processing; the getter documents null as "no processor is used".
  • The constructor, setQueryConfigHandler and parse all skip a null processor; the package doc calls processing optional.
  • Where the class refuses null (setSyntaxParser, setQueryBuilder), it says "cannot be null" and throws IllegalArgumentException, not an NPE.
The full argument246 words
  • The contract allows null twice and says what it means: "The argument can be null, which means that no processor will be used to process the query node tree" (QueryParserHelper.java:92-93) and "@param processor ... this argument can be null" (:95-96). The getter: "it returns null if 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 null everywhere else: constructor (:71), setQueryConfigHandler (:189), parse (:218).
  • Null handling was chosen setter by setter: setSyntaxParser and setQueryBuilder document "cannot be null" (:106-107, :123-124) and throw IllegalArgumentException (:115-117, :132-134); setQueryConfigHandler documents "It can be null" (:177-178) and accepts it.
  • Tests and history: no test passes null to 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 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). 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.

Suggested fix

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.

Guard the call, as the constructor does (validated by the checks) (recommended)

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 645d4fe14a20 with 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:checkGoogleJavaFormat passes with the fix.
  • Standalone runner with the fixed class shadowed: the repro passes with -ea and -da.
  • The hunt's 150-test flexible-parser subset passes stock and with this guard and the hunt's two other fix sketches (in QueryNodeImpl and QueryNodeProcessorPipeline) applied together; the 534-test module run is the one with this guard alone.

Change the contract to "cannot be null"

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 note

Downstream products are affected only if their own setup code passes null to this setter; query text never reaches it.

How it was found

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.

Prior reports, history and checks

Prior reports

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.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub, mailing lists, JIRA archive, web (search script)2026-10-2851 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 run166 distinct hits, none about this bug
GitHub (apache/lucene, all of GitHub, Elasticsearch, OpenSearch, Solr, Lucene.NET)2026-10-2823 manual searches (setQueryNodeProcessor, QueryParserHelper with null or NullPointerException, "this.processor"); the file lists of all 414 open apache/lucene PRsonly #14168 names the setter; no open PR touches QueryParserHelper.java
GitHub refresh (apache/lucene)2026-10-30issues 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.javaonly #14168 and older unrelated issues; no open PR touches the flexible parser; the newest commit on the file is the 2021 reformat

History

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.

Versions and branches executed, with results
Version or commitVerdictResult
2.4.1 API absentno 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.0NPE in setQueryNodeProcessor (portable main() repro); the constructor control passes
9.1.0, 10.0.0 affectedNPE at QueryParserHelper.java:102 (the finder's JUnit)
9.12.3, 10.5.2, 10.6.0 (4ae946e5a9b9) affectedthe same NPE with the minimal test (Maven Central jars, -da)
all 147 final releases 2.9.0 to 10.5.2 bytecode scanno null test before the call; setQueryConfigHandler has one (control, from the fact check's re-scan)
main 645d4fe14a20 affectedthe same NPE, gradle and standalone, assertions on and off
upstream main 2f424fd428da, branch_10_6 source readlines 100-103 unchanged (2026-10-30)
main 645d4fe14a20 + null guard fixthe minimal test passes; the whole queryparser suite: 534 tests, 0 failures, 1 skipped

Independent checks

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.

Each independent check and its verdict
CheckVerdictWhat it found
finder (code reading, then a JUnit) candidateFails 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 realOwn 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, minorBuilt 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, highRe-ran the independent reproduction's JUnit (-ea, -da: 2 of 4 fail at QueryParserHelper.java:102) and the finder's.
credibility audit keepOwn main() repro: the NPE through StandardQueryParser and PrecedenceQueryParser; the constructor control parses.
this doc's runs (2026-10-30) reproduced; fix validatedThe 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.

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,098 words

Finding record

LFV-968, snapshot of 2026-11-06

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 whole lucene/queryparser test suite with the fix and without it), and on the standalone runner with -ea and -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 main a9779ef3c193, where QueryParserHelper.java is identical. Upstream main 2f424fd428da and branch_10_6 were read on 2026-10-30. Only Linux aarch64 and OpenJDK 25.0.3 were run.

Lucene and upstream sources

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-223
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/package-info.java:25-26, :36-42
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:231, :235-242
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/precedence/PrecedenceQueryParser.java:35, :41, :50
  • lucene/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, :50
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/StandardQueryNodeProcessorPipeline.java:45-70
  • lucene/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-223
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/package-info.java:25-26, :36-42
  • lucene/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

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

Repro files

About the diagrams

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.