All findings · Lucene finding · LFV-546 · uh-maxnohighlightpassages-below-minus-one-throws

UnifiedHighlighter with maxNoHighlightPassages below -1 throws 'Illegal Capacity' when a document's field text has no match, although the javadoc says any value below 0 falls back to maxPassages

The javadoc says any maxNoHighlightPassages below 0 falls back to maxPassages, but the code maps only exactly -1. With -2, any document whose field has text but no match makes the highlight call throw IllegalArgumentException: Illegal Capacity: -2 instead of returning its first sentence.

Summary

Priority
Low, rank 649A maxNoHighlightPassages below -1 makes the UnifiedHighlighter throw on the first no-match document, a rare-value crash like LFV-438, LFV-265 and LFV-493, so it closes the highlighter rare-value group.
Impact
wrong error
Since
6.3.0 (2016, the first release with the UnifiedHighlighter)introduced 2016-10-04 · over 10 years
Affected
every release from 6.3.0 to 10.6.0, and main
Status
documented
Found by
checking the highlighter against a reference written from its javadoc

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) 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

When a hit has no match in the highlighted field, the UnifiedHighlighter returns a summary of the field's first sentences; maxNoHighlightPassages says how many. The code turns only exactly -1 into maxPassages. Any other negative value is used as the count itself, and the summary method then asks the JDK for a list with that negative capacity.

Background: the no-match summary

  1. The UnifiedHighlighter cuts a field into passages (by default, sentences) and returns the best ones that contain a query match.
  2. If a document is a hit but nothing in the field matches, it returns a summary instead: the field's first sentences. maxNoHighlightPassages sets how many. By the javadoc, any value below 0 (the default is -1) means "as many as maxPassages", 0 means no summary (null), and a positive value is the count.
Summary of the bug. An application highlights the field body, text First sentence. Second sentence., for a query that matches no word in it, with maxPassages 1 and maxNoHighlightPassages -2, and expects the default summary First sentence., as with -1. The javadoc says any value below 0 falls back to maxPassages; FieldHighlighter line 93 maps only exactly -1, so -2 reaches line 112, new ArrayList<>(Math.min(-2, 10)), and the JDK throws IllegalArgumentException: Illegal Capacity: -2. Values -1, 0, 1 and 2 behave as documented. The fix: < 0 instead of == -1.
The summary card: the application's call with maxNoHighlightPassages = -2, what the javadoc promises for values below 0 against what line 93 does, the negative list capacity at line 112, what the caller sees, and the one-character fix. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Index one document, body = First sentence. Second sentence., and search MatchAllDocsQuery: a hit, but no word in body matches. highlight("body", query, topDocs, 1) with the default maxNoHighlightPassages (-1) returns the summary First sentence. .
  2. Build the highlighter with withMaxNoHighlightPassages(-2). You expect the same First sentence. : the javadoc says any value below 0 means "use maxPassages" (here 1); only 0 means no summary.
  3. You get IllegalArgumentException: Illegal Capacity: -2. -3, Integer.MIN_VALUE, the deprecated setter and a subclass returning -5 fail the same way; in a two-field highlightFields call the field that did match loses its snippet too.
  4. Why: FieldHighlighter.java:93 swaps in maxPassages only for exactly -1, so -2 reaches new ArrayList<>(Math.min(-2, 10)) at line 112, and the JDK rejects a negative capacity. < 0 instead of == -1 fixes it.

Who is affected

Code that sets maxNoHighlightPassages to a negative value other than -1, through the builder, the deprecated setter or an override of the protected getter, for example -2 or Integer.MIN_VALUE as "unset". Every highlight call that includes a document whose field has text but no match then fails. The default -1 works, and the failure is loud.

Mechanism, terms and numbers612 words

When a document is a hit but nothing in the highlighted field matches, the UnifiedHighlighter returns a default summary: the field's first sentences. The javadoc of getMaxNoHighlightPassages says how many: "If it's less than 0 (the default) then this defaults to the maxPassages parameter given for each request. If this is 0 then the resulting highlight is null (not formatted)." (UnifiedHighlighter.java:634-639 on main 645d4fe14a20). The code maps only exactly -1, the default: getSummaryPassagesNoHighlight(maxNoHighlightPassages == -1 ? maxPassages : maxNoHighlightPassages) (FieldHighlighter.java:91-93). Any other negative value is used as the sentence count, and in the stock implementation the list allocation at the top of getSummaryPassagesNoHighlight, new ArrayList<>(Math.min(maxPassages, 10)) (:112, right after an assert), throws IllegalArgumentException: Illegal Capacity: -2 from the JDK. With withMaxNoHighlightPassages(-2), Integer.MIN_VALUE, the deprecated setter or an override of the protected getter, every highlight call that includes a document whose field has text but no match fails, with assertions on and off; in a multi-field call the other fields lose their snippets too. Documents with a match are highlighted normally. The check, the capacity line and the javadoc all came in the first UnifiedHighlighter commit (LUCENE-7438, 2016); every release from 6.3.0 to 10.6.0 and main is affected. A one-character change, < 0 instead of == -1, fixes it.

The code (main 645d4fe14a20, lucene/highlighter/.../uhighlight/)

  • Where the value comes from. The default is DEFAULT_MAX_HIGHLIGHT_PASSAGES = -1 (UnifiedHighlighter.java:116). Builder#withMaxNoHighlightPassages(int) stores any int, with no check and no javadoc (:401-404), as does the deprecated setMaxNoHighlightPassages (:216-219). The protected getMaxNoHighlightPassages(String field) returns it (:640-642) under the contract quoted above (:634-639), and getFieldHighlighter passes it to each FieldHighlighter (:1138).
  • The sentinel check. FieldHighlighter.highlightFieldForDoc returns null at once for empty content (FieldHighlighter.java:77-79; a document without a stored value is skipped earlier, UnifiedHighlighter.java:968-969); otherwise it scores passages (:86), and when none matched (:89) it calls getSummaryPassagesNoHighlight(maxNoHighlightPassages == -1 ? maxPassages : maxNoHighlightPassages) (:91-93). Only exactly -1 becomes maxPassages.
  • The throw. The stock getSummaryPassagesNoHighlight(int maxPassages) (protected, :109-128) first asserts that the break iterator is at the start (:110; it is on the stock path, and the -ea runs pass it), then allocates new ArrayList<>(Math.min(maxPassages, 10)) (:112). Math.min(-2, 10) = -2, and the JDK's ArrayList(int) throws IllegalArgumentException("Illegal Capacity: -2").
  • Why 0 and positive values work. Math.min(0, 10) = 0 gives an empty list, the loop while (passages.size() < maxPassages) (:115) does not run, and :96-99 returns null, the documented 0 behaviour; positive values collect that many sentences.
  • Reach. highlight and highlightFields end in highlightFieldsAsObjects, whose loop calls highlightFieldForDoc (UnifiedHighlighter.java:987-988) with no per-field or per-document catch, so one unmatched document fails the whole call. highlightWithoutSearcher reaches the same method (:1105-1107).

What each call does on main (same document, maxPassages = 1 unless stated; assertions on and off)

callmain todayexpected
withMaxNoHighlightPassages(-1) (default)First sentence. same
withMaxNoHighlightPassages(0)nullsame
withMaxNoHighlightPassages(1), (2)First sentence. , First sentence. Second sentence.same
-2, -3, Integer.MIN_VALUEIllegalArgumentException: Illegal Capacity: -2 / -3 / -2147483648First sentence.
-1 and -2 with maxPassages = 2two sentences / Illegal Capacity: -2two sentences
deprecated setMaxNoHighlightPassages(-2)Illegal Capacity: -2First sentence.
subclass overriding getMaxNoHighlightPassages to return -5Illegal Capacity: -5First sentence.
-2, query body:second (the document matches)<b>Second</b> sentence.same
highlightFields({"title", "body"}, title:lucene, topDocs, {1, 1}) with -1title <b>Lucene</b> highlighting, body First sentence. same
the same with -2the whole call throws Illegal Capacity: -2; the title snippet is lost tooboth snippets
Exposure and likelihood181 words
  • Who is hit. Code that configures the UnifiedHighlighter with a negative maxNoHighlightPassages other than -1: Builder#withMaxNoHighlightPassages, the deprecated setMaxNoHighlightPassages, or a subclass overriding the protected getMaxNoHighlightPassages hook; for example a developer who reads "less than 0 (the default)" and uses -2 or Integer.MIN_VALUE, or an application that forwards an unchecked configuration value.
  • Default vs opt-in. The default (-1) works; the trigger is a non-default configuration value. Then, with the stock FieldHighlighter, any document whose field has text but no match triggers it, with any index and any offset source.
  • What goes wrong. The call throws instead of returning the summary; in a multi-field call all fields lose their snippets. The message names a JDK list capacity, not the parameter. No wrong results, corruption or hang.
  • Scale. None needed: one document, one call; deterministic, assertions on or off.
  • Workaround. Use -1 for "same as maxPassages".
  • How likely. Rare: someone has to pick a negative value other than -1, the failure is loud on the first unmatched document, and the recorded searches found no report in ten years.

Minimal repro

One JUnit method on the public API, in the highlighter's test package: index one two-sentence document, search MatchAllDocsQuery so body has no match, build the highlighter with withMaxNoHighlightPassages(-2) and expect First sentence. . No randomness; it fails with assertions on and off and passes with the fix.

TestNegativeMaxNoHighlightPassages.java (lucene/highlighter, package org.apache.lucene.search.uhighlight, 1 test)59 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.search.uhighlight;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestNegativeMaxNoHighlightPassages extends LuceneTestCase {

  public void testMinusTwoDefaultsToMaxPassages() throws Exception {
    StandardAnalyzer analyzer = new StandardAnalyzer();
    try (Directory dir = new ByteBuffersDirectory()) {
      try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig(analyzer))) {
        Document doc = new Document();
        doc.add(new TextField("body", "First sentence. Second sentence.", Field.Store.YES));
        w.addDocument(doc);
      }
      try (DirectoryReader r = DirectoryReader.open(dir)) {
        IndexSearcher searcher = new IndexSearcher(r);
        Query query = new MatchAllDocsQuery(); // no match in "body"
        UnifiedHighlighter highlighter =
            UnifiedHighlighter.builder(searcher, analyzer).withMaxNoHighlightPassages(-2).build();
        String[] snippets;
        try {
          snippets = highlighter.highlight("body", query, searcher.search(query, 1), 1);
        } catch (IllegalArgumentException e) {
          throw new AssertionError(
              "maxNoHighlightPassages -2 (< 0): expected the default summary, got " + e, e);
        }
        assertEquals("First sentence. ", snippets[0]);
      }
    }
  }
}
Command
cp $REPRO/TestNegativeMaxNoHighlightPassages.java $LUCENE/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/
cd $LUCENE && ./gradlew :lucene:highlighter:test --tests org.apache.lucene.search.uhighlight.TestNegativeMaxNoHighlightPassages -Ptests.seed=DEADBEEF
Expected
First sentence. , the same summary the default -1 gives.
Observed
IllegalArgumentException: Illegal Capacity: -2 from new ArrayList<>(Math.min(-2, 10)) at FieldHighlighter.java:112.

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

More files, environment, full output263 words

Copy the test into lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/ of a Lucene checkout and run it with gradle (command below). It uses StandardAnalyzer, ByteBuffersDirectory, a plain IndexWriterConfig and IndexSearcher, and no random(), so the outcome does not depend on the seed (LuceneTestCase still picks its default codec and locale from it). The IllegalArgumentException is caught only to rethrow it as the cause of an AssertionError that states expected vs actual; the cause keeps the ArrayList and FieldHighlighter frames. With the < 0 fix it passes unchanged, so it doubles as the regression test.

Executed: gradle on a clean 645d4fe14a20 (fails) and with the fix (passes); the standalone runner on the stock main jars with -ea and -da, and on the 10.6.0 release jars (fails, same cause); the minimization audit also ran a second seed (5 of 5 fail).

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): fails; also fails on the 10.6.0 release jars (and the hunt's tests on 9.12.3 and 10.5.2)
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux aarch64 (plain Java collections, nothing platform-specific)
Command (environment)
cp $REPRO/TestNegativeMaxNoHighlightPassages.java $LUCENE/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/
cd $LUCENE && ./gradlew :lucene:highlighter:test --tests org.apache.lucene.search.uhighlight.TestNegativeMaxNoHighlightPassages -Ptests.seed=DEADBEEF

Observed output

# gradle, main 645d4fe14a20, -Ptests.seed=DEADBEEF (the test as shown)
TestNegativeMaxNoHighlightPassages > testMinusTwoDefaultsToMaxPassages FAILED
    java.lang.AssertionError: maxNoHighlightPassages -2 (< 0): expected the default summary, got java.lang.IllegalArgumentException: Illegal Capacity: -2
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:9E98FFBEE072D62E]:0)
        at org.apache.lucene.search.uhighlight.TestNegativeMaxNoHighlightPassages.testMinusTwoDefaultsToMaxPassages(TestNegativeMaxNoHighlightPassages.java:52)

        Caused by:
        java.lang.IllegalArgumentException: Illegal Capacity: -2
            at java.base/java.util.ArrayList.<init>(ArrayList.java:161)
            at org.apache.lucene.search.uhighlight.FieldHighlighter.getSummaryPassagesNoHighlight(FieldHighlighter.java:112)
            at org.apache.lucene.search.uhighlight.FieldHighlighter.highlightFieldForDoc(FieldHighlighter.java:92)
            at org.apache.lucene.search.uhighlight.UnifiedHighlighter.highlightFieldsAsObjects(UnifiedHighlighter.java:988)
            at org.apache.lucene.search.uhighlight.UnifiedHighlighter.highlightFields(UnifiedHighlighter.java:858)
            at org.apache.lucene.search.uhighlight.UnifiedHighlighter.highlightFields(UnifiedHighlighter.java:836)
            at org.apache.lucene.search.uhighlight.UnifiedHighlighter.highlight(UnifiedHighlighter.java:769)
            at org.apache.lucene.search.uhighlight.TestNegativeMaxNoHighlightPassages.testMinusTwoDefaultsToMaxPassages(TestNegativeMaxNoHighlightPassages.java:50)

# standalone runner, seed DEADBEEF
main 645d4fe14a20  -ea / -da   tests_run=1 failures=1   same cause
10.6.0             -ea         tests_run=1 failures=1   same cause

Why it looks unintended

  • The javadoc covers every negative value: "If it's less than 0 (the default) then this defaults to the maxPassages parameter given for each request."
  • That sentence and the == -1 check came in the same 2016 commit; no test, changelog or discussion limits the sentinel to -1.
  • Intended rejection looks different: withMaxLength rejects bad values up front, with a message.
The full argument223 words
  • The written contract covers every negative value. getMaxNoHighlightPassages: "If it's less than 0 (the default) then this defaults to the maxPassages parameter given for each request. If this is 0 then the resulting highlight is null (not formatted)." (UnifiedHighlighter.java:634-639). The setters have no javadoc of their own. The highlight javadoc promises the same fallback: "If no highlights were found for a document, the first maxPassages sentences from the field will be returned." (:759-761).
  • Not a later narrowing. The javadoc sentence, the == -1 check and the capacity line all arrived in LUCENE-7438 (722e82712435, 2016-10-04); no test, CHANGES or MIGRATE entry restricts the sentinel to -1, and neither #8490 nor its review discusses other negative values.
  • Intended rejection looks different. withMaxLength and setMaxLength reject bad values up front with "maxLength must be < Integer.MAX_VALUE" (:355-362, :191-199). Here nothing validates; the error is an incidental JDK capacity check, thrown late (only when an unmatched document is reached) and naming neither the parameter nor the cause.
  • The same module treats a documented -1 as "any negative". TokenStreamFromTermVector documents "-1 disables the limit" and implements maxStartOffset < 0 ? Integer.MAX_VALUE : maxStartOffset (TokenStreamFromTermVector.java:83-87).
  • Tests. Upstream tests pass only 0 (TestUnifiedHighlighter.java:843, TestUnifiedHighlighterTermIntervals.java:673) or the default; no test covers another negative value either way.
The devil's-advocate check163 words

The strongest case for "works as designed" (the cross-model lenses, the independent reproducer, the devil's-advocate verifier and the adjudication): the default constant is exactly -1 and the check has been == -1 from the start; Solr's override returns exactly -1 (by code reading); the setters carry no javadoc; UnifiedHighlighter is @lucene.experimental and FieldHighlighter @lucene.internal; a negative sentence count other than -1 has no natural meaning; nobody reported it in ten years.

Why it fails: the only written description of the value explicitly covers every value below 0 and came in the same commit as the check; nothing in the code, tests or history says -1 alone; and even if -1 were meant to be the only sentinel, the result should be a clear up-front rejection, not a late JDK exception that only fires for unmatched documents and takes the other fields of the call down with it. Either reading leaves a defect: the code and the javadoc disagree. Verdict: real, low impact.

Suggested fix

Recommended Map every negative value to maxPassages (one character)

Treat every negative value as the default: maxNoHighlightPassages < 0 ? maxPassages : maxNoHighlightPassages at FieldHighlighter.java:93 (one character). -1, 0 and positive values behave as before; -2 now gives First sentence. . With it the repro and all 723 highlighter-module tests pass.

        passages =
            getSummaryPassagesNoHighlight(
                maxNoHighlightPassages < 0 ? maxPassages : maxNoHighlightPassages);
All options and trade-offs2 options · 488 words

Two options. The < 0 check matches the only written contract and was validated with gradle in a scratch worktree at 645d4fe14a20; making -1 the documented sentinel and rejecting other negatives up front is the alternative a maintainer may prefer (not executed). Either way, do not fix only the capacity line (for example Math.max(0, Math.min(maxPassages, 10)) at :112): the loop while (passages.size() < maxPassages) (:115) then never runs for a negative count, so -2 would silently return null, the documented 0 behaviour, still contradicting the javadoc (code reading). A natural combination: the < 0 check plus one sentence of javadoc on Builder#withMaxNoHighlightPassages that repeats the getter's contract.

Map every negative value to maxPassages (one character) (validated by the checks) (recommended)

In FieldHighlighter.highlightFieldForDoc, compare with < 0 instead of == -1.

        passages =
            getSummaryPassagesNoHighlight(
                maxNoHighlightPassages < 0 ? maxPassages : maxNoHighlightPassages);

Trade-offs

  • Stock FieldHighlighter: -1 unchanged; every other negative value now gives the first maxPassages sentences, as the javadoc says; 0 and positive values unchanged. No API change.
  • It validates nothing, so a subclass override of the getter returning a negative value is treated as the default too, which is what the javadoc promises.
  • Custom extensions are the one thing to consider: the mapping happens before the protected getSummaryPassagesNoHighlight(int) hook, so a custom FieldHighlighter that overrides that hook and relies on receiving a raw negative value other than -1 would now get maxPassages (upstream's extensibility test subclasses FieldHighlighter from outside the package, TestUnifiedHighlighterExtensibility.java:167-194, :259-284; code reading).
  • It does not cover a negative maxPassages request argument, which has no documented range and is not claimed here: with the default -1 such a value reaches the same line 112, with or without the fix (code reading).

What the checks validated

  • Gradle, own scratch worktree at 645d4fe14a20, JDK 25.0.3, -Ptests.seed=DEADBEEF (2026-10-30): without the fix the minimal test fails (Illegal Capacity: -2); with it, :lucene:highlighter:test --tests 'org.apache.lucene.search.uhighlight.*' passes 471 of 471 tests in 14 suites (the package's 470 upstream tests plus the minimal test), and the whole :lucene:highlighter:test passes 723 of 723 tests in 35 suites, 0 failures, 0 errors.
  • Earlier shadow runs on ea57360912ed: the finder's test, an independent reproducer's 7 tests, the other verifiers' tests and the audit's test pass with it. The diagram probe on 645d4fe14a20 with the fix returns First sentence. for -2, -3, Integer.MIN_VALUE, the deprecated setter and the -5 override, and both snippets for the two-field call.

Make -1 the documented sentinel and reject other negatives up front

Change the getter javadoc to say -1 (the default), and throw IllegalArgumentException with a message for values below -1 in Builder#withMaxNoHighlightPassages and setMaxNoHighlightPassages, as withMaxLength does. Not executed.

Trade-offs

A clear, early error, consistent with withMaxLength. But it narrows the documented contract, and a subclass that overrides getMaxNoHighlightPassages bypasses the setter check, so FieldHighlighter.java:93 (or getFieldHighlighter) would need a check too.

Downstream note

By code reading of one default-branch revision each (not exhaustive), the default implementations of Solr (which passes -1 or 0), Elasticsearch and OpenSearch (which pass 0 or 1 and replace the summary method) do not pass such a value, so the known exposure is applications that set it themselves.

How it was found

Bounded-exhaustive translation validation: the highlighter's passage and summary code was compared with a reference written from its javadoc, on every short text up to a size bound. Writing the reference exposed the == -1 check; the exhaustive run then showed every no-match call with -2 throwing, and a public-API test confirmed it.

The method in full146 words

Method: bounded-exhaustive translation validation (the hunt's semi-formal tier), in a hunt unit on the UnifiedHighlighter's passage layer, run on the stock main jars at ea57360912ed.

  • A reference implementation of passage splitting and the default summary was written from the javadoc (for the summary: N = maxPassages when maxNoHighlightPassages < 0), plus the oracle "no exception on legal input".
  • The harness compared the real jar with it on every text of up to 5 code points over the alphabet {a, b, space, '.', U+1D400, '<', '&'}, 7 break iterators, maxPassages 1 to 3 and maxNoHighlightPassages -2 to 2.
  • The first signal was code reading: the finder noticed the == -1 check while writing the reference. The exhaustive run witnessed it: every no-match call with -2 threw Illegal Capacity: -2 (604,380 per iterator). A hand-written public-API JUnit confirmed it, later reduced to the single test shown here.

Prior reports, history and checks

Prior reports

No prior report found (searched 2026-10-26, re-checked 2026-10-30) in GitHub issues, PRs and commits including the JIRA archive, the Lucene and Solr mailing lists, downstream projects or the web. Related, not this bug: #8490 (the introducing change), #11234 / PR #412 (the Builder) and SOLR-13121 (a different parameter).

Searches4 searches

Related, not this bug: #8490 / LUCENE-7438 (the introducing change; nobody discusses other negative values), #11234 / PR #412 / LUCENE-10197 (added the unvalidated Builder#withMaxNoHighlightPassages, first released in 9.1.0; 56 review comments, none on the value range) and SOLR-13121 (an open request that a negative hl.maxAnalyzedChars mean "unlimited", a different parameter). Coverage limits: GitHub search reads the first 50 results per query; phrase search for "Illegal Capacity" returned 0 everywhere, which fits no report but cannot rule out phrase-search limits; attachments, chat and private reports cannot be searched.

What was searched, when, and what turned up
WhereWhenHowFound
Search tool (GitHub issues/PRs/commits incl. the JIRA archive, dev@, java-user@, issues@, commits@, downstream, web)2026-10-26three runs: maxNoHighlightPassages, setMaxNoHighlightPassages, getSummaryPassagesNoHighlight, "Illegal Capacity", negative and default-summary phrasings, DEFAULT_MAX_HIGHLIGHT_PASSAGES; the predecessor and downstream angle (getEmptyHighlight, noMatchSize, hl.defaultSummary)54, 192 and 200 hits; 20 classified, none about this bug
GitHub (apache/lucene, apache/lucene-solr, apache/solr), JIRA and the mailing lists2026-10-2610 manual GitHub searches with positive controls, 10 JQL queries (LUCENE and SOLR), list-archive searches on the Lucene and Solr lists; downstream code search in Solr, Elasticsearch and OpenSearch#8490, #11234 / #412, SOLR-13121: related, none the same bug
Open PRs2026-10-26changed files of all 409 open apache/lucene PRsno PR touches FieldHighlighter.java
GitHub, re-check2026-10-30issue/PR search for maxNoHighlightPassages, withMaxNoHighlightPassages, getSummaryPassagesNoHighlight, "Illegal Capacity" with highlighter and UnifiedHighlighter, defaultSummary negative; open PRs mentioning FieldHighlighter; FieldHighlighter.java on upstream main (twice)only #10135 (a different bug); 0 open PRs; line 93 unchanged on upstream main 2f424fd428da

History

Introduced with the UnifiedHighlighter itself (LUCENE-7438, 2016) and first released in 6.3.0: the javadoc and the == -1 check disagree from the first commit. Every release from 6.3.0 to 10.6.0 and main is affected; not fixed upstream.

Versions tested6 rows

Introducing commit: 722e82712435 on master and 4b6794368df on branch_6x (2016-10-04). The == -1 test was already on the review branch (apache/lucene-solr#79, closed without a GitHub merge; the work was committed directly). Later commits changed only the layout of the check line: LUCENE-8145 (9422609a5d4) re-indented it and LUCENE-9570 (6faa4f98e04) re-wrapped it; nothing touched the capacity line. The Builder setter came in 9.1.0 (LUCENE-10197, #412) without validation; 9.0.0 has only the setter. Not a regression. Old releases ran on JDK 25.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedthe minimal test fails (gradle; standalone -ea and -da)
10.6.0 affectedthe minimal test fails (release jars); the tag carries the same check and capacity line
9.1.0, 9.12.3, 10.5.2 affectedthe finder's Builder JUnit fails (9.1.0 is the first release with the Builder)
6.3.0 to 10.5.2 11 releases runan old-API main() probe fails on 6.3.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.9.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0 and 10.5.2; the -1 control is correct in every run; all 78 release tags carry the code
6.2.1 and older not affectedno UnifiedHighlighter (the probe does not compile); the predecessor had no such option
< 0 fix (scratch worktree at 645d4fe14a20) not affectedthe minimal test passes, and so do all 723 highlighter-module tests (gradle, seed DEADBEEF)

Independent checks

An independent reproducer, an intended-behaviour devil's advocate, a review by a model from another family (reading only), an adjudication, a fresh-seed re-run and a credibility audit all confirm it; the version sweep and gradle runs with and without the fix complete the record.

Checks and verdicts372 words

The cross-model reviewer could not run Java, so its refute-lens review voted not real at low confidence, on procedure only, while agreeing with the root cause and rating the intended-behaviour argument weak; a re-run of the repro failed. All execution evidence comes from Claude-based AI tools and separate runs. The downstream reach is by code reading only.

Each independent check and its verdict
CheckVerdictWhat it found
finder (bounded-exhaustive translation validation) candidateEvery no-match call with -2 threw in the exhaustive run; its JUnit fails on main (-ea, -da), 10.5.2 and 9.12.3, and passes with the fix.
cross-model review (OpenAI GPT-5.6, refute and intended lenses, reading only) refute: procedural "not real" (low); intended: real (medium)Could not run Java; the lenses agreed on the root cause, found the inputs in contract and rated the intended-behaviour argument weak; separate runs re-ran the repro (fails).
independent reproducer realOwn 7-test JUnit outside the package fails 5 of 7 on main (-ea, -da), 10.5.2 and 9.12.3; the -1 and 0 controls pass; its main() fails on 8.11.4; its fix passes 7 of 7.
intended-behaviour devil's advocate realBuilt the works-as-designed case and rejected it; its JUnit fails on main and 10.5.2 through the builder, the deprecated setter and an override, and passes with the fix.
adjudication confirmed (high)Re-ran the reproducer's JUnit with a new seed: 5 of 7 fail with -ea and -da, 7 of 7 pass with the fix; impact class wrong error.
registry re-run and credibility audit keepFresh seeds: the finder's and the reproducer's JUnits fail and pass with the fix; the audit's own index-free JUnit fails and passes 3 of 3 with the fix.
history sweep affected since 6.3.0Old-API probe on 11 releases 6.3.0 to 10.5.2 and main, the Builder JUnit on 9.1.0 to 10.5.2, a 78-tag code check.
this doc gradle repro and fix checkThe minimal test with gradle on a clean 645d4fe14a20 (fails) and with the < 0 fix (passes; the uhighlight package, 471 tests, and the whole highlighter module, 723 tests, pass); the stock main jars (-ea, -da) and the 10.6.0 jars (fail); read-only GitHub re-checks.

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

Finding record

LFV-546, snapshot of 2026-11-06

Registry title
UnifiedHighlighter with maxNoHighlightPassages below -1 throws IllegalArgumentException 'Illegal Capacity' for every document with no match in the field, although the javadoc says any value below 0 falls back to maxPassages
Area
highlighter / UnifiedHighlighter no-match default summary (FieldHighlighter.highlightFieldForDoc maps only '== -1' to maxPassages; FieldHighlighter.getSummaryPassagesNoHighlight)
Priority
Low, rank 649: A maxNoHighlightPassages below -1 makes the UnifiedHighlighter throw on the first no-match document, a rare-value crash like LFV-438, LFV-265 and LFV-493, so it closes the highlighter rare-value group.
Impact class
wrong error
Impact note
Applications on any release from 6.3.0 through 10.6.0 or on main that configure the UnifiedHighlighter with a maxNoHighlightPassages value below -1 (Builder#withMaxNoHighlightPassages, the deprecated setMaxNoHighlightPassages, or an override of the protected getMaxNoHighlightPassages hook), which the getter's javadoc says means 'use maxPassages', get IllegalArgumentException 'Illegal Capacity: N' from the JDK ArrayList constructor instead of the default summary for every document with no match in the highlighted field, and by code reading the whole multi-field call fails; documents with a match are highlighted normally, nothing is corrupted, there is no hang, no index size is needed, and Solr (-1 or 0 only), Elasticsearch and OpenSearch (0 or 1, with their own summary method) never pass such a value (code reading).
Summary
You build a UnifiedHighlighter with withMaxNoHighlightPassages(-2), or any other negative value except -1, relying on the getMaxNoHighlightPassages javadoc: 'If it's less than 0 (the default) then this defaults to the maxPassages parameter given for each request.' For a document with no match in the field you expect the same default summary as with -1, for example 'First sentence. '. Instead FieldHighlighter maps only exactly -1 to maxPassages and passes -2 on to getSummaryPassagesNoHighlight, whose new ArrayList<>(Math.min(-2, 10)) throws IllegalArgumentException 'Illegal Capacity: -2', so the highlight call fails.
Introduced
722e82712435ecf46c9868137d885484152f749b (LUCENE-7438 'New UnifiedHighlighter', committed directly by dsmiley; issue now apache/lucene#8490; the work of apache/lucene-solr#79 by Timothy055, which GitHub shows as closed unmerged) and its branch_6x backport 4b6794368df373df1f68ccf27f7556914efeb95e [read: git log -S MaxNoHighlightPassages over the whole repository finds nothing older; git log -L shows later edits to the check line only by 9422609a5d4 (LUCENE-8145, indentation) and 6faa4f98e04 (LUCENE-9570, reformat), and to the ArrayList line by none]. The '== -1' check (FieldHighlighter.java:93 today), the ArrayList capacity line (:112) and the 'less than 0 (the default)' javadoc all arrived in this commit, so there is no regression window; the '== -1' test already existed on the PR branch (f0340e27, 2016-09-04) (2016-10-04)
First affected version
6.3.0 [exec] (first release with the UnifiedHighlighter, from the branch_6x backport 4b6794368df); 6.2.1 has no org.apache.lucene.search.uhighlight package [exec: compile error] and its predecessor PostingsHighlighter had no maxNoHighlightPassages option [read]; first release from master containing 722e827: 7.0.0 [exec]
Affected versions
every release that has the UnifiedHighlighter: 6.3.0-6.6.6, 7.0.0-7.7.3, 8.0.0-8.11.4, 9.0.0-9.12.3, 10.0.0-10.6.0, and main ea57360912ed [exec: the follow-up's old-API main() probe fails on 6.3.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.9.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da), with the -1 control correct in every run; the finder's Builder JUnit fails on 9.1.0, 9.12.3, 10.5.2 and main (-ea, -da); the independent reproduction's JUnit fails on main (-ea, -da), 10.5.2 and 9.12.3 and its main() on 8.11.4; the intended-behaviour check's on main (-ea, -da) and 10.5.2] [read: all 78 release tags 6.3.0 to 10.5.2 in the history clone carry the check, the capacity line and the javadoc]; not fixed upstream [read, not executed: FieldHighlighter.java:93 unchanged on upstream main, branch_10x, branch_10_6 and branch_10_5 on 2026-10-26, and no open PR touches FieldHighlighter.java]; the 10.6.0 release jars (4ae946e5a9b) fail the doc's minimal JUnit with -ea [exec, 2026-10-30]
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
semi-formal: exhaustive check
Upstream and prior reports
none found (2026-10-26, the prior-report record, plus the finder's run: three runs of the scripted prior-report search over apache/lucene issues and PRs, commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web (54, 192 and 200 hits, none this bug); 10 manual GitHub searches on apache/lucene, the archived apache/lucene-solr and apache/solr for 'maxNoHighlightPassages', 'getSummaryPassagesNoHighlight' and four 'Illegal Capacity' phrasings, 0 hits each, with positive controls that do return hits; downstream code search in Solr, Elasticsearch and OpenSearch; list-archive searches of the Lucene and Solr lists; 10 JIRA queries over LUCENE and SOLR; 9 web queries; the changed files of all 409 open apache/lucene PRs; 20 hits classified, all same_bug false); not fixed upstream (read, not executed: FieldHighlighter.java:93 still reads '== -1' on main, branch_10x, branch_10_6 and branch_10_5, Builder#withMaxNoHighlightPassages still assigns without a check on main and branch_10x, CHANGES.txt and MIGRATE.md never mention maxNoHighlightPassages, and no open PR touches FieldHighlighter.java); closest items, not reports: #8490 / LUCENE-7438 (the introducing change; nobody discusses other negative values), #412 / LUCENE-10197 (added the Builder setter without validation; 56 review comments, none on the value range) and SOLR-13121 (an open Solr request that negative hl.maxAnalyzedChars mean unlimited, a different parameter)
Lucene commit tested
main 645d4fe14a20 (11.0.0-SNAPSHOT): the minimal test with gradle in a clean scratch worktree (fails), and with the < 0 fix applied (passes, as do the uhighlight package's 470 other tests and the whole highlighter module, 723 tests in all, seed DEADBEEF); the standalone runner on the stock jars (-ea, -da: fails); the 10.6.0 release jars (fails). A probe made for the diagram ran the value table, the deprecated setter, a subclass override and a two-field call on 645d4fe14a20 with and without the fix. The hunt and its verifiers ran on main ea57360912ed, where the whole lucene/highlighter tree is identical, and on the 9.12.3 and 10.5.2 jars; the history sweep ran an old-API probe on 11 releases from 6.3.0 to 10.5.2 and the Builder test on 9.1.0. FieldHighlighter.java:93 is unchanged on upstream main 2f424fd428da (2026-10-30).

Lucene and upstream sources

Finding record
  • lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/FieldHighlighter.java:89-94 (at ea57360912ed) (no passages -> getSummaryPassagesNoHighlight(maxNoHighlightPassages == -1 ? maxPassages : maxNoHighlightPassages)), :104-108 (javadoc: 'By default this just returns the first maxPassages sentences'), :109-112 (getSummaryPassagesNoHighlight: new ArrayList<>(Math.min(maxPassages, 10)))
  • lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java:116 (at ea57360912ed) and :153 (DEFAULT_MAX_HIGHLIGHT_PASSAGES = -1), :216-219 (deprecated setMaxNoHighlightPassages, no validation), :355-362 (withMaxLength validates up front; contrast), :401-404 (Builder#withMaxNoHighlightPassages, no validation), :634-642 (getMaxNoHighlightPassages javadoc: 'If it's less than 0 (the default) then this defaults to the maxPassages parameter given for each request. If this is 0 then the resulting highlight is null')
  • lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighter.java:843 (at ea57360912ed) and TestUnifiedHighlighterTermIntervals.java:673 (the only explicit values in upstream tests are 0), visibility/TestUnifiedHighlighterExtensibility.java:146-147 (pass-through override)
  • lucene/highlighter/src/java/org/apache/lucene/search/highlight/TokenStreamFromTermVector.java:83-87 (at ea57360912ed) (a sibling API documents -1 but treats every negative value as the sentinel)
The finding in 30 seconds
  • a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/FieldHighlighter.java:41, :59, :73-102 (highlightFieldForDoc; :77-79 empty content returns null, :86, :89, :91-93 the '== -1' mapping, :96-99), :104-108 (javadoc), :109-128 (getSummaryPassagesNoHighlight; :110 the assert, :112 the ArrayList capacity, :115 the loop)
  • a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java:116, :153, :216-219, :282, :401-404, :475, :634-642, :766-770, :858, :968-969 (a missing stored value is skipped), :987-988, :1105-1107, :1138, :1143-1160
Who is affected
  • a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java:116, :216-219, :401-404, :634-642, :987-988
Why it looks unintended
  • a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java:94, :191-199, :355-362, :401-404, :634-642, :759-761
  • a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/FieldHighlighter.java:32, :91-93, :105-106, :112
  • a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/highlight/TokenStreamFromTermVector.java:83-87
  • a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighter.java:843, TestUnifiedHighlighterTermIntervals.java:673, visibility/TestUnifiedHighlighterExtensibility.java:146-147 (grep MaxNoHighlightPassages)
Suggested fix
  • a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/FieldHighlighter.java:93, :112, :115, :96-99 (why a capacity-only fix returns null)
  • a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java:355-362 (withMaxLength validates), :634-642, :924 (maxPassages passed on unchecked), :1138
  • a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/FieldHighlighter.java:104-109 (the protected, overridable summary hook); lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/visibility/TestUnifiedHighlighterExtensibility.java:167-194, :259-284 (FieldHighlighter subclassed out of package)
History
  • a Lucene clone: git show releases/lucene/10.6.0:.../FieldHighlighter.java (lines 93, 112 unchanged); releases/lucene/9.0.0 and 9.1.0:.../UnifiedHighlighter.java (withMaxNoHighlightPassages only in 9.1.0)
Diagrams
  • a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/FieldHighlighter.java:91-93, :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 in parallel with this doc from the code at 645d4fe14a20 and an executed probe on the stock main jars (assertions on and off) and with the fix.

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.