All findings · Lucene finding · LFV-027 · flexible-parser-fractional-slop-nfe

Flexible StandardQueryParser throws NumberFormatException on a fractional phrase slop such as "a b"~30.5

The flexible StandardQueryParser throws an unchecked NumberFormatException for a phrase with a fractional slop such as "a b"~30.5, instead of the slop-30 phrase that Lucene 9.0.0 and the classic parser return, or the QueryNodeException that parse() declares. Every release since 9.1.0 does this.

Summary

Priority
Low, rank 891The flexible parser throws an unchecked NumberFormatException instead of a parse error on input like "a b"~30.5, an occasional user habit, but only the error type is wrong.
Impact
wrong error
Since
9.1.0 (LUCENE-10223, 2021)introduced 2021-11-11 · about 4 years
Affected
9.1.0 to 10.6.0 and main
Status
documented
Found by
fuzzing the query parsers (Jazzer)

AI-assisted page: this finding was first signalled by a testing baseline (randomized, fuzz, property-based or stress testing, or static analysis) 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 parser's lexer accepts 30.5 as a number after ~, but since 9.1.0 the phrase rule converts it with Integer.parseInt.

Background: phrase slop and two of the parsers

  1. A phrase query "a b" matches the two words next to each other. A slop ~N lets them be up to N positions apart: "a b"~30 allows 30.
  2. Lucene ships several query parsers. Two of them share this syntax: the classic QueryParser and the flexible StandardQueryParser, which borrows most of it. Each declares one checked exception for bad query text: ParseException and QueryNodeException.
Summary card of the bug. You do: parse "a b"~30.5 with the flexible StandardQueryParser and catch QueryNodeException. Expected: a PhraseQuery "a b"~30 or a QueryNodeException; on main: java.lang.NumberFormatException: For input string: "30.5". 8.11.4 and 9.0.0 return slop 30; 9.1.0 and later throw. Inside Lucene main, three steps: 1 the lexer splits the query into <QUOTED> "a b", <TILDE> ~ and <NUMBER> 30.5; 2 the QuotedTerm rule calls Integer.parseInt("30.5"), which throws NumberFormatException; 3 parse() handles only ParseException and Error, so the exception escapes. 9.0.0 used (int) Float.parseFloat("30.5") and got 30. Footer: introduced by f725b27e12a (LUCENE-10223, PR #429), first released in 9.1.0; the same exception for a slop too large for an int ("a b"~3000000000) and at the other NUMBER-to-parseInt sites, e.g. (a b)@1.5 and fn:atLeast(1.5 a b); fix direction: parseInt throws ParseException, or the slop goes back to the 9.0.0 truncation.
How "a b"~30.5 goes through the flexible parser: the lexer's NUMBER token, the phrase rule's Integer.parseInt, and the NumberFormatException that parse() does not handle. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Parse "a b"~30 with new StandardQueryParser(new StandardAnalyzer()): you get a PhraseQuery with slop 30.
  2. Parse "a b"~30.5. You expect slop 30, which Lucene 9.0.0 and the classic QueryParser return, or at worst the QueryNodeException that parse() declares, which you get for "a b"~30x.
  3. You get java.lang.NumberFormatException: For input string: "30.5". The lexer reads 30.5 as one number token, and the phrase rule converts it with Integer.parseInt.
  4. parse() turns syntax errors into QueryNodeException, but this unchecked exception passes through. An application that catches QueryNodeException to report a bad query fails on this input instead.
  5. The same helper reads "a b"~3000000000, (a b)@1.5 and the numbers in fn: functions, with the same result, with assertions on or off, since 9.1.0.

Who is affected

Applications that pass user-typed query text to the flexible StandardQueryParser (not the classic QueryParser), with default settings. A fractional or oversized number after a phrase's ~, after @, or in an fn: argument makes parse() throw an undeclared exception. Only the exception type is wrong; no result changes. Rare input; no public report since 9.1.0.

Mechanism, terms and numbers438 words

The flexible StandardQueryParser documents proximity search as "test failure"~4 and declares QueryNodeException as the failure of parse(). Its lexer has one NUMBER token for integers and decimals, so 30.5 after ~ is a legal token, but since LUCENE-10223 (9.1.0) the phrase rule converts it with Integer.parseInt. parse("\"a b\"~30.5", "f") therefore throws java.lang.NumberFormatException: For input string: "30.5", which parse() does not convert, while Lucene 8.11.4, 9.0.0 and today's classic QueryParser return PhraseQuery "a b"~30.

The same helper serves the @N operator and the integer arguments of the fn: interval functions, and fails the same way for a fraction or for an integer above Integer.MAX_VALUE. Nothing is searched and no result is wrong: a query string is refused with an undeclared exception type. A one-helper grammar fix (throw the declared ParseException) is validated with the whole queryparser test suite and check; restoring the 9.0.0 truncation for the slop is validated as an alternative.

The bug, step by step

  1. Lexer. <NUMBER: (<_NUM_CHAR>)+ ( "." (<_NUM_CHAR>)+ )? > (StandardSyntaxParser.jj:160): one token type for integers and decimals, shared by the phrase slop, the fuzzy similarity (term~0.7), boosts, @N and the fn: arguments. "a b"~30.5 lexes as <QUOTED> <TILDE> <NUMBER>.
  2. Phrase rule. QuotedTerm reads ( <TILDE> slop = <NUMBER> { q = new SlopQueryNode(q, parseInt(slop)); } )? (.jj:833; generated StandardSyntaxParser.java:1293).
  3. Helper. public static int parseInt(Token token) { return Integer.parseInt(token.image); } (.jj:119-121; generated :109-111). Integer.parseInt("30.5") throws NumberFormatException, and so does Integer.parseInt("3000000000").
  4. Entry point. parse() (.jj:99-113) passes a ParseException on (:103; it is a QueryNodeParseException) and wraps an Error into one (:106). NumberFormatException is a RuntimeException, so it passes through parse(), QueryParserHelper.parse and StandardQueryParser.parse unchanged.

Where else

parseInt has 12 calls at 10 places, all on NUMBER tokens: the slop (.jj:833), @N (:361), and fn:atLeast, fn:maxwidth, fn:maxgaps, fn:within, fn:extend (two), fn:notWithin, fn:wildcard and fn:fuzzyTerm (two). The study ran 10 such inputs on main 7cecf79523, for example (a b)@1.5, fn:maxwidth(2.5 a) and "a b"~3000000000 (the slop site with a value too large for an int): all 10 throw NumberFormatException. The fact check re-ran all 12 calls on 645d4fe14a20 (13 inputs): each throws.

The contrast

In the same grammar, FuzzyOp reads its NUMBER with parseFloat and checks it, throwing ParseException for a bad value (.jj:712-728), so term~1.1 is a clean QueryNodeException. "a b"~30x and "a b"~-1 are ParseExceptions too. Only the parseInt sites let a lexed number fail with an unchecked exception.

Exposure and likelihood164 words
  • Who is hit. Applications that pass query strings to the flexible StandardQueryParser. Default parser settings, any analyzer, no index needed. The classic QueryParser is not affected.
  • What goes wrong. Code that catches QueryNodeException to answer "invalid query" gets an unexpected NumberFormatException instead. What happens next depends on the application; one that catches nothing else answers that request with a generic server error.
  • When. A number after ~ on a phrase with a fraction or above Integer.MAX_VALUE, and decimals in @N and in the integer arguments of fn: functions. Decimals after ~ are normal on terms (term~0.7, the old fuzzy-similarity form, still accepted), so the same habit on a phrase hits this. Users who relied on the 9.0.0 truncation got the exception on upgrade.
  • Limits. Deterministic and immediate. No wrong results, no hang, no resource growth, no index effect. The registry rates the input rare, and no public report exists in the four and a half years since 9.1.0 (released 2022-03-22).

Minimal repro

One JUnit test in lucene/queryparser, public API only: the flexible parser parses "a b"~30.5. It accepts a query or a QueryNodeException; an escaping NumberFormatException fails it as an AssertionError with that cause. No index; deterministic; fails with assertions on and off.

TestFractionalPhraseSlop.java (1 test)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.queryparser.flexible.core.QueryNodeException;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestFractionalPhraseSlop extends LuceneTestCase {

  public void testFractionalPhraseSlop() {
    StandardQueryParser parser = new StandardQueryParser(new StandardAnalyzer());
    try {
      parser.parse("\"a b\"~30.5", "f");
    } catch (QueryNodeException e) {
      // expected: a Query or this declared QueryNodeException; actual: NumberFormatException
    } catch (NumberFormatException e) {
      throw new AssertionError("expected a Query or a QueryNodeException, got " + e, e);
    }
  }
}
Command
cd $LUCENE   # an apache/lucene checkout at 645d4fe14a20 (or any commit with the same StandardSyntaxParser)
cp $REPRO/TestFractionalPhraseSlop.java lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/
./gradlew :lucene:queryparser:test --tests org.apache.lucene.queryparser.flexible.standard.TestFractionalPhraseSlop -Ptests.seed=DEADBEEF
# assertions off fails the same way: add -Ptests.asserts=false
Expected
PhraseQuery "a b"~30 (as in 9.0.0 and the classic parser), or a QueryNodeException.
Observed
java.lang.NumberFormatException: For input string: "30.5" from StandardSyntaxParser.parseInt, on main, 10.6.0, 10.5.2 and 9.1.0, with assertions on and off.

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

More files, environment, full output205 words

The test sits in the parser's test package, as Lucene's own parser tests do, but uses no package-private member. Nothing in it is randomized; seeds DEADBEEF and 5EED1234 give the same failure.

As a regression test it is the same test, except that ecjLintTest rejects the unused catch parameter e and accepts it written as catch (@SuppressWarnings("unused") QueryNodeException e), as QueryParserBase.java:803-805 writes it. With that change it fails on stock, passes with the recommended fix and passes :lucene:queryparser:check -x test.

The other shapes of the same root cause ("a b"~3000000000, (a b)@1.5, the fn: arguments) are left out of the test.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): affected; also the 10.6.0, 10.5.2 and 9.1.0 release jars with this test
jdkOpenJDK 25.0.3
osLinux, aarch64

Observed output

TestFractionalPhraseSlop > testFractionalPhraseSlop FAILED
    java.lang.AssertionError: expected a Query or a QueryNodeException, got java.lang.NumberFormatException: For input string: "30.5"
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:62F18F6838A133AC]:0)
        at org.apache.lucene.queryparser.flexible.standard.TestFractionalPhraseSlop.testFractionalPhraseSlop(TestFractionalPhraseSlop.java:32)

        Caused by:
        java.lang.NumberFormatException: For input string: "30.5"
            at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
            at java.base/java.lang.Integer.parseInt(Integer.java:565)
            at java.base/java.lang.Integer.parseInt(Integer.java:662)
            at org.apache.lucene.queryparser.flexible.standard.parser.StandardSyntaxParser.parseInt(StandardSyntaxParser.java:110)
            at org.apache.lucene.queryparser.flexible.standard.parser.StandardSyntaxParser.QuotedTerm(StandardSyntaxParser.java:1293)
            at org.apache.lucene.queryparser.flexible.standard.parser.StandardSyntaxParser.Term(StandardSyntaxParser.java:1256)
            ...
            at org.apache.lucene.queryparser.flexible.standard.parser.StandardSyntaxParser.parse(StandardSyntaxParser.java:91)
            at org.apache.lucene.queryparser.flexible.core.QueryParserHelper.parse(QueryParserHelper.java:214)
            at org.apache.lucene.queryparser.flexible.standard.StandardQueryParser.parse(StandardQueryParser.java:279)
            at org.apache.lucene.queryparser.flexible.standard.TestFractionalPhraseSlop.testFractionalPhraseSlop(TestFractionalPhraseSlop.java:28)
:lucene:queryparser:test (FAILURE): 1 test, 1  failure

# the same message on 10.6.0 (StandardSyntaxParser.java:111 <- :1294), 10.5.2 and 9.1.0 (:110 <- :1063), assertions on and off

Why it looks unintended

  • parse() declares QueryNodeException, and the grammar already rejects "a b"~30x and term~1.1 that way (an upstream test asserts the latter).
  • A regression: 9.0.0 and the classic QueryParser return slop 30 for "a b"~30.5; the 9.1 migration note does not mention slop.
  • It came as a side effect of new interval-function helpers; the PR and the issue never discuss slop.
The full argument333 words
  • The declared failure. StandardQueryParser#parse and QueryParserHelper#parse: "@throws QueryNodeException if something wrong happens along the three phases" (StandardQueryParser.java:274, QueryParserHelper.java:211). StandardSyntaxParser#parse: "@throws ParseException if the parsing fails" (.jj:96). SyntaxParser#parse declares only QueryNodeParseException (SyntaxParser.java:29). ParseException extends QueryNodeParseException, which extends QueryNodeException.
  • Other bad numbers are rejected cleanly. "a b"~30x and "a b"~-1 give ParseException; FuzzyOp throws ParseException for term~1.1, and TestQPHelper.java:560 asserts it: assertQueryNodeException("term~1.1"); // value > 1, throws exception. testException (:1002-1010) asserts QueryNodeException for malformed text such as "\"some phrase".
  • A regression that no note announced. 9.0.0 converted the slop with (int) Float.parseFloat(slop.image) (.jj:445 at releases/lucene/9.0.0) and returns slop 30; before LUCENE-9528 the conversion sat in catch (Exception ignored). f725b27e12a (LUCENE-10223, PR #429) swapped in parseInt(slop) in the same diff that added the parseInt/parseFloat helpers for the interval functions. The PR has no description; its only review comments are a static-analysis bot's findings in two new node classes and the author's reply telling the bot to ignore one. Issue #11259 never mentions slop. The 9.1 migration note lists only fn: and @ as parsing differently (MIGRATE.md:1116-1122).
  • The classic parser, whose syntax the flexible one "borrows most of" (StandardQueryParser.java:63-65), accepts the input as slop 30 (QueryParserBase.java:798-807).
  • A contrast in another parser. #12251 / PR #12277 (9.9.0) first proposed wrapping the NumberFormatException that RegExp's Integer.parseInt throws for an oversized repeat count. That part was dropped because, as rmuir commented, "NumberFormatException is a subclass of IllegalArgumentException already", and IllegalArgumentException is what RegExp declares (RegExp.java:510-512). The merged change only adds a clean IllegalArgumentException for an out-of-order repetition range (mikemccand: "we catch an invalid usage and throw a clean exception"); RegExp.java:1274 and :1279 still call Integer.parseInt unwrapped. So the raw NumberFormatException was left alone there because it already is the declared type. Here the declared exception is the checked QueryNodeException, and NumberFormatException is not a subtype of it.
The devil's-advocate check204 words

The strongest case (rated moderate by the cross-model reviewer, and raised by the prior-report search): slop is an int everywhere (SlopQueryNode, PhraseQuery), the docs show only integer slops, so "a b"~30.5 is outside the documented syntax and any exception is a fair answer; @throws QueryNodeException does not promise that no RuntimeException escapes; and the XML query parser even has a test asserting that NumberFormatException escapes for an empty slop attribute (TestCoreParser.java:163-167, open issue #12708 is about that test).

Why it fails: the claim does not need the input to be accepted, and the test passes on a QueryNodeException too. For a text parser, "outside the syntax" means a syntax error, and this grammar routes syntax errors, even Error, to QueryNodeParseException and already rejects term~1.1 and "a b"~30x that way. 9.0.0 accepted the input and no migration note announced the change. The XML parser takes an XML document (CoreParser.parse(InputStream), CoreParser.java:141), not query-syntax text, and declares its own ParserException; its test pins what one malformed attribute throws today rather than stating a rule, so it is a weak precedent for a text grammar. A maintainer may still prefer rejection to truncation: that is the recommended fix.

Suggested fix

Recommended Make parseInt throw ParseException (one helper, all 12 calls)

Make the grammar's parseInt helper throw the declared ParseException when the number is not an int (+7/-2 in StandardSyntaxParser.jj, then regenerate). It covers the slop, @N and every fn: argument. Validated: the repro passes, all 534 queryparser tests pass, check passes. Alternative, also validated: restore the 9.0.0 truncation for the slop only.

-  public static int parseInt(Token token) {
-    return Integer.parseInt(token.image);
+  public static int parseInt(Token token) throws ParseException {
+    try {
+      return Integer.parseInt(token.image);
+    } catch (NumberFormatException e) {
+      throw new ParseException(
+          new MessageImpl(QueryParserMessages.INVALID_SYNTAX, "expected an integer: " + token.image));
+    }
   }
All options and trade-offs4 options · 680 words

Both fixes go into the grammar; the generated parser is then rebuilt with ./gradlew :lucene:queryparser:regenerateFlexibleStandardSyntaxParser, which formats it and updates the checksum file. Both were validated in a scratch worktree of 645d4fe14a20 (OpenJDK 25.0.3, seed DEADBEEF), removed afterwards. The choice between them is the maintainers': reject a fractional slop, or accept it as 9.0.0 did.

Make parseInt throw ParseException (one helper, all 12 calls) (validated by the checks) (recommended)

In StandardSyntaxParser.jj (:119-121). After regeneration the change is 3 files, +101/-90: the grammar, the generated parser (most of its lines are JavaCC renaming its lookahead methods, whose names carry .jj line numbers) and the checksum file. Every NUMBER that is not an int (a fraction, or more than Integer.MAX_VALUE) becomes the declared ParseException; integers parse as today. For all 12 calls this follows from the shared helper; the shadow probe below ran three of the inputs.

-  public static int parseInt(Token token) {
-    return Integer.parseInt(token.image);
+  public static int parseInt(Token token) throws ParseException {
+    try {
+      return Integer.parseInt(token.image);
+    } catch (NumberFormatException e) {
+      throw new ParseException(
+          new MessageImpl(QueryParserMessages.INVALID_SYNTAX, "expected an integer: " + token.image));
+    }
   }

Trade-offs

  • "a b"~30.5, accepted by 9.0.0, is now rejected; since 9.1.0 it has failed anyway, only with the wrong type.
  • parseInt is public static, so the added checked throws ParseException changes its signature for any outside caller; within Lucene only the generated parser calls it. A new private helper would avoid that.
  • The visible message is the generic Syntax Error, cannot parse "a b"~30.5: , the same as for "a b"~30x: parse() calls setQuery on every ParseException (.jj:104), and setQuery replaces the message (QueryNodeParseException.java:55-58). The "expected an integer" text is therefore lost; a plain new ParseException() behaves the same. Keeping details is the older open #7934.
  • Could add a CHANGES entry.

What the checks validated

  • Stock: the repro fails with assertions on and off. Fix: JavaCC regeneration succeeds; the repro passes with assertions on and off.
  • The whole :lucene:queryparser:test: 534 tests (the module's own plus the repro), 1 skipped, 0 failures.
  • With the lint-clean regression test in place, :lucene:queryparser:check -x test passes: the three parser-checksum checks, checkGoogleJavaFormat, ecjLintMain, ecjLintTest, forbiddenApisMain, forbiddenApisTest, checkLicenses.
  • Shadowed onto the 645d4fe14a20 jars, the regenerated parser turns "a b"~30.5, "a b"~3000000000 and (a b)@1.5 into ParseException; "a b"~30 still gives slop 30.

Restore the 9.0.0 truncation for the phrase slop (one line) (validated by the checks)

In QuotedTerm (.jj:833); after regeneration 3 files, +4/-4. "a b"~30.5 gives slop 30 and "a b"~3000000000 slop 2147483647, as in 9.0.0 and the classic parser.

-  ( <TILDE> slop = <NUMBER> { q = new SlopQueryNode(q, parseInt(slop)); } )?
+  ( <TILDE> slop = <NUMBER> { q = new SlopQueryNode(q, (int) Float.parseFloat(slop.image)); } )?

Trade-offs

Silent truncation (30.5 becomes 30), which is what users had before 9.1.0. It fixes only the slop: (a b)@1.5 and the fn: arguments still throw NumberFormatException. Combined with the recommended fix for the helper, it gives 9.0.0 compatibility for the slop and the declared exception elsewhere; that combination was not run.

What the checks validated

The repro passes; the whole :lucene:queryparser:test: 534 tests, 1 skipped, 0 failures. Shadowed onto the jars: slop 30 and 2147483647 as above, (a b)@1.5 still NumberFormatException. A first regeneration in the same worktree left the unchanged generated files unformatted (the formatter task is incremental); it was discarded and redone with the formatter forced to re-run.

Catch NumberFormatException in parse()

Add a catch (NumberFormatException e) next to the Error branch (.jj:106-112) and wrap it in a QueryNodeParseException. Not run.

Trade-offs

Keeps the helper's signature and covers every call, but also hides a NumberFormatException from any other code reached during parsing, and sits further from the cause.

An integer-only token after a phrase's ~ (not recommended)

Not run.

Trade-offs

The lexer cannot tell a phrase slop from a fuzzy similarity (term~0.7 needs the decimal), so this needs lexical states or grammar restructuring for a small gain.

Downstream note

Not a Lucene security issue; downstream, a service that catches only QueryNodeException to answer "invalid query" returns a generic server error for such input instead.

How it was found

An AI tool wrote and ran a Jazzer fuzzer over Lucene's query parsers that checks each parser throws only its documented exception; it flagged this NumberFormatException without a hint. An independent verifier then reproduced it on main, 9.1.0 and 10.1.0 and traced it to LUCENE-10223.

The method in full145 words

Method: non-formal baseline of the study (fuzzing).

  • The fuzzer, QueryParserFuzzer, fed generated strings to the classic, flexible and simple query parsers. Its oracle: each parser may only throw its documented exception, and a parsed query must survive toString, equals, count and rewrite. It ran for 1.5 + 7 + 20 minutes, about 470,000 executions, and produced five query-parser findings; this is one of them. It was a blind find.
  • An independent AI verifier (same model family) wrote its own JUnit test and a cross-version main(): the test failed on main 7cecf79523, 9.1.0 and 10.1.0; 9.0.0 accepted the input as slop 30; all 10 parseInt inputs failed. It found the introducing commit by reading the history.
  • Later steps cut the repro to one test (2026-10-29 noise audit), re-ran it on 645d4fe14a20, swept the releases, and validated two fixes with gradle.

Prior reports, history and checks

Prior reports

No prior report found: 174 queries over GitHub issues and PRs, the JIRA archive, the dev@ and user lists, downstream projects and the web, plus the changed files of all 410 open PRs (2026-10-31). The introducing PR #429 and its issue #11259 never mention slop.

Searches5 searches

Verdict new. The closest items are the introducing change, a different symptom of the same change, a counter-precedent in another parser and a contrasting case in RegExp; none reports this bug.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues and PRs (apache/lucene)2026-10-3192 queries (70 by the search tool, 22 manual), e.g. NumberFormatException StandardQueryParser, StandardSyntaxParser parseInt, fractional slop, decimal slop, SlopQueryNode, "For input string" queryparser, created:>2021-11-01 scansno report; closest: #11259 / PR #429 (the introducing change), #11800 (@ became syntax in 9.1, a declared parse exception), #12708 (the XML parser's NumberFormatException test), #12251 / PR #12277 (RegExp contrast), #7934 (parse() drops error details), #16820 (proposed deprecation of the flexible parser)
Open PRs2026-10-31the changed-file lists of all 410 open PRsnone touches StandardSyntaxParser.jj, StandardSyntaxParser.java or StandardQueryParser.java
JIRA archive (LUCENE)2026-10-3115 queries, e.g. text ~ "QueryNodeParseException", queryparser component and NumberFormatException after 2021-11-01no report
Mailing lists2026-10-3140 queries over dev@, java-user@, issues@ and commits@no report
Downstream and web2026-10-31all of GitHub for "StandardSyntaxParser.parseInt"; Elasticsearch, OpenSearch, Solr, Lucene.NET; 6 web queriesno report; one downstream NumberFormatException from StandardQueryParser on Lucene 6.6 had an application cause

History

Introduced in 9.1.0 by LUCENE-10223 (#429, f725b27e12a, merged 2021-11-11; shipped as ff9ee28c603), which swapped the slop's (int) Float.parseFloat for Integer.parseInt. 8.11.4 and 9.0.0 accept the input; the 33 releases from 9.1.0 to 10.6.0 have the code (6 of them run, all failing) and main fails.

Versions tested7 rows

Introduced by LUCENE-10223 "interval support in standard syntax parser" (apache/lucene#429, issue #11259), by dweiss, merged 2021-11-11 as f725b27e12a on main and backported to branch_9x as ff9ee28c603, first released in 9.1.0 (2022-03-22). It replaced (int) Float.parseFloat(slop.image) with parseInt(slop) and added the helper; on main, git log -S'parseInt(slop)' finds no other commit (with --all it also lists the backport). Later grammar edits: f6cebac3337 (LUCENE-10414: fn:fuzzyTerm and an optional maxExpansions argument for fn:wildcard, three more parseInt calls, which with the 9 of f725b27e12a make the 12) and 1f4da953f0c (license header). All 33 release tags from 9.1.0 to 10.6.0 have both lines (the releases not run are affected by this reading); 9.0.0 has neither. The @N and fn: sites are new syntax in 9.1.0, so only the phrase slop is a regression.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedNumberFormatException, assertions on and off (gradle and the standalone runner)
10.6.0, 10.5.2 affectedNumberFormatException, assertions on and off (release jars, SHA-1 checked)
10.0.0, 9.12.3 affectedNumberFormatException (history probe, assertions off)
9.1.0 first affectedNumberFormatException, assertions on and off
9.0.0, 8.11.4 not affectedPhraseQuery "a b"~30 (slop 30); 9.0.0 with assertions on and off
9.1.0 to 10.6.0, all 33 tags readthe slop action parseInt(slop) and the helper are present in the grammar and the generated parser
fix options on 645d4fe14a20 not affectedreject: ParseException; truncate: slop 30; the repro and all 534 queryparser tests pass with each

Independent checks

An independent verifier reproduced it in the study; a 2026-10-31 reproducer and a tie-breaker re-ran it on main, 10.6.0, 10.5.2 and 9.1.0 and rated it real (high). The review by another model family (OpenAI GPT-5.6) could run nothing, so its "not real" is procedural (a separate run re-ran the supplied test, which failed); no independently written cross-model reproduction exists.

Checks and verdicts236 words

The cross-model reviewer agreed on the root cause and argued that a decimal slop is outside the documented syntax (moderate); the tie-breaker answered that argument (see the devil's advocate above).

Each independent check and its verdict
CheckVerdictWhat it found
Study verifier new bugOwn JUnit test and cross-version main(): fails on main 7cecf79523, 9.1.0 and 10.1.0; 9.0.0 accepts; 10 of 10 parseInt inputs fail.
Public-list rerun and main refresh still reproducesThe study test fails the same 2 of 4 methods on b83101cdd2 and a9779ef3c193.
Noise audit kept, low impactCut the repro to one test, one input; fails with seeds DEADBEEF and 5EED1234, assertions on and off.
Reproducer on 645d4fe14a20 realMain and 10.5.2 fail with assertions on and off; not fixed on main.
Cross-model review, refute lens (OpenAI GPT-5.6) not real (medium), reading onlyCould run no command, so its verdict is procedural; agreed on the root cause. A separate run, not the reviewer, re-ran the supplied test on 10.5.2, and it failed. Two earlier attempts also ran nothing.
Tie-breaker real (high)Re-ran main, 10.6.0, 10.5.2 and 9.1.0 and a test-framework-free probe; 9.0.0 accepts; every not-real ground fails.
This doc's fix validation both fixes passGradle in a scratch worktree: the repro fails on stock and passes with each fix; 534 queryparser tests pass with each; check passes with the recommended fix.

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 notes483 words

Finding record

LFV-027, snapshot of 2026-11-06

Registry title
Flexible StandardQueryParser throws NumberFormatException on a fractional phrase slop
Area
queryparser / flexible
Priority
Low, rank 891: The flexible parser throws an unchecked NumberFormatException instead of a parse error on input like "a b"~30.5, an occasional user habit, but only the error type is wrong.
Impact class
wrong error
Impact note
Applications that let users type phrase slops into the flexible StandardQueryParser; one input shape; released since 9.1.0.
Summary
You parse a phrase with a fractional slop such as "a b"~30.5 with the flexible StandardQueryParser. You expect it to be accepted, as 9.0.0 did by truncating the slop to 30, or rejected with a parse error. Instead, since 9.1.0, parse() throws NumberFormatException.
Introduced
f725b27e12a (LUCENE-10223, PR #429) (2021-11-11)
First affected version
9.1.0
Affected versions
9.1.0+ [exec]; 9.0.0 accepts it [exec]
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed in the earlier formal verification study by independent AI checks and a rerun on Lucene main; also a cross-model review that could only read the code and voted “not real” while agreeing with the root cause (the study's other checks rated it real); not yet triaged by a person.
Found by
non-formal baseline: fuzzing/PBT
Upstream and prior reports
none found (study searches 2026-10-22; public-list check 2026-10-20..23)
Lucene commit tested
main 645d4fe14a20 (gradle and the standalone runner, assertions on and off); the release jars 8.11.4 and 9.0.0 (not affected), 9.1.0, 9.12.3, 10.0.0, 10.5.2 and 10.6.0 (affected); the study's runs on main 7cecf79523, b83101cdd2 and a9779ef3c193 and on 10.1.0; both fix options regenerated with JavaCC in a scratch worktree of 645d4fe14a20. Upstream main 2f424fd428da (2026-10-30) has the same code.

Lucene and upstream sources

The finding in 30 seconds
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.jj:99-113, (at 645d4fe14a20) 119-121, 160, 361, 420, 432, 444, 585, 601, 617, 655, 676-677, 712-728, 833
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.java:88-103, (at 645d4fe14a20) 109-111, 1293 (generated)
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:63-65, (at 645d4fe14a20) 83 (background)
Who is affected
  • lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/TestQPHelper.java:542 (at 645d4fe14a20) (term~0.7 accepted)
  • Lucene main 645d4fe14a20 dev-tools/doap/lucene.rdf:289-291 (9.1.0 created 2022-03-22)
Why it looks unintended
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:63-65, (at 645d4fe14a20) 274; core/QueryParserHelper.java:211; core/parser/SyntaxParser.java:29; standard/parser/StandardSyntaxParser.jj:96, 712-728
  • lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/TestQPHelper.java:560, (at 645d4fe14a20) 1002-1010; lucene/queryparser/src/test/org/apache/lucene/queryparser/xml/TestCoreParser.java:163-167
  • lucene/MIGRATE.md:1116-1122; (at 645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:798-807
  • lucene/core/src/java/org/apache/lucene/util/automaton/RegExp.java:510-512, (at 645d4fe14a20) 1274, 1279; lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/CoreParser.java:141
  • apache/lucene PR #12277 comments by rmuir and mikemccand (read-only); merged commit 4b3f7662ce8 (RegExp.java: the out-of-order repetition range check only)
Suggested fix
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/QueryNodeParseException.java:55-58; (at 645d4fe14a20) standard/parser/StandardSyntaxParser.jj:104, 106-112

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

Repro files

About the diagrams

Made from the code at 645d4fe14a20 and the executed runs; re-rendered on 2026-10-31 after the diagram check (the summary card's footer now files "a b"~3000000000 under the slop site), and this alt text checked against that render.

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.