All findings · Lucene finding · LFV-738 · uax29-ftp-url-without-path-not-recognized

UAX29URLEmailTokenizer splits an ftp URL that has no path: ftp://host is not one URL token, http://host is

UAX29URLEmailTokenizer keeps http://localhost and ftp://ftp.example.com/ as one <URL> token, but splits ftp://ftp.example.com into ftp plus the host, because its ftp rule requires a / path that RFC 1738 makes optional. Every release since 3.1.0 has it; a one-line grammar fix keeps the URL whole.

Summary

Priority
Low, rank 518An ftp URL with no path is a rare input, and the opt-in UAX29URLEmailTokenizer only loses the single URL token while the host stays searchable.
Impact
silent wrong results
Since
3.1.0 (LUCENE-2167, 2010), the first release with UAX29URLEmailTokenizerintroduced 2010-09-28 · over 16 years
Affected
every release 3.1.0 to 10.5.2, 10.6.0 (by reading), and main
Status
documented
Found by
bounded-exhaustive check of the tokenizer against an RFC 1738 / RFC 3986 reference

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

UAX29URLEmailTokenizer is the URL- and e-mail-aware sibling of StandardTokenizer: it keeps a whole URL as one <URL> token. Its http rule accepts a URL with no path; its ftp rule does not, so ftp://host is cut after the scheme and the rest is typed by whatever rule matches next.

Diagram of the bug: UAX29URLEmailTokenizer's http rule accepts a URL with no path, so http://localhost is one URL token, while its ftp rule requires a path that starts with '/', so ftp://localhost is split into the words ftp and localhost.
How a path-less ftp URL and the same URL over http go through the tokenizer's URL rules, and the tokens each one produces. Select the still to open it at full size. Generated with AI, checked against the code.
  1. Tokenize get it from ftp://ftp.example.com today with UAX29URLEmailTokenizer, UAX29URLEmailAnalyzer or the uax29UrlEmail factory, default settings.
  2. You expect 5 tokens, with ftp://ftp.example.com as one <URL>: the javadoc promises URL tokenization "according to the relevant RFCs", RFC 1738 lets an ftp URL omit its path, and the same tokenizer keeps http://localhost and ftp://ftp.example.com/ whole.
  3. You get 6: ftp <ALPHANUM> and ftp.example.com <URL>. Other hosts fare worse: ftp://localhost gives two words, ftp://192.0.2.10 a <NUM>, and ftp://anonymous@ftp.example.org the <EMAIL> //anonymous@ftp.example.org.
  4. The cause is one grammar line: the ftp rule requires a path starting with / (UAX29URLEmailTokenizerImpl.jflex:177,180), while the http rule beside it makes the path optional (:167). With no URL match, the scanner falls back to its word, number, host and e-mail rules.
  5. No error. An exact TermQuery for ftp://ftp.example.org finds 0 of 1 documents and a <URL> filter misses the link; a phrase query analyzed the same way still finds 1 of 1. A trailing / avoids it; with the fix the term query finds 1 of 1.

Who is affected

Users of UAX29URLEmailTokenizer, UAX29URLEmailAnalyzer or the uax29UrlEmail factory (opt-in, not the default analyzer), on every release since 3.1.0, whose text has ftp links without even a trailing /. Exact-URL terms and <URL>-token extraction miss them; host words and same-analyzer phrase queries still match. Rare input; a fix needs a reindex.

Mechanism, terms and numbers653 words

UAX29URLEmailTokenizer (also used by UAX29URLEmailAnalyzer and the uax29UrlEmail factory) keeps URLs and e-mail addresses whole and says it tokenizes them "according to the relevant RFCs"; its grammar cites RFC 1738 and RFC 3986. RFC 1738 says of the ftp path: "The whole url-path may be omitted, including the "/" delimiting it". The grammar's http rule makes the path optional (UAX29URLEmailTokenizerImpl.jflex:167), but its ftp rule requires a path that starts with / (:177, :180). So http://localhost and ftp://ftp.example.com/ are one <URL> token each, while ftp://ftp.example.com becomes ftp <ALPHANUM> + ftp.example.com <URL>, ftp://localhost two words, ftp://192.0.2.10 ftp + a <NUM>, and ftp://anonymous@ftp.example.org ftp + an <EMAIL> token //anonymous@ftp.example.org, with no error. An exact term lookup of such a URL finds nothing and <URL>-token extraction misses it; a phrase query analyzed the same way still matches. The rule dates from the 2010 URL grammar (LUCENE-2167) and every release from 3.1.0 to 10.6.0 (10.6.0 by reading) and main has it. Making the path optional (({FTPorFILEpath} {FTPtype}?)?) and regenerating the scanner fixes it; the change alters tokens of path-less ftp URLs, so affected fields need a reindex.

Terms

  • UAX29URLEmailTokenizer: Unicode word breaking (UAX#29) plus URL and e-mail recognition, generated by JFlex from UAX29URLEmailTokenizerImpl.jflex. Used by UAX29URLEmailAnalyzer and the uax29UrlEmail factory. Not the default: IndexWriterConfig defaults to StandardAnalyzer, whose tokenizer has no <URL> type.
  • Path: the part after the host, starting with /. RFC 1738 makes it optional for ftp (section 3.2.2) and required for file URLs (fileurl = "file://" [ host | "localhost" ] "/" fpath).

The rules (main a9779ef3c193)

jflex:162  URIauthorityLoose = {URIlogin}? {URIhostLoose} {URIport}?
jflex:167  HTTPurlFull = {HTTPscheme} {URIlogin}? {URIhostLoose} {URIport}? {HTTPpath}? {URIquery}? {URIfragment}?
jflex:177  FTPorFILEpath = "/" {FTPorFILEsegment} ("/" {FTPorFILEsegment})*
jflex:180  FTPurl = {FTPscheme} {URIauthorityLoose} {FTPorFILEpath} {FTPtype}? {URIfragment}?
jflex:183  FILEurl = {FILEscheme} {URIhostLoose}? {FTPorFILEpath} {URIfragment}?
jflex:185  URL = {HTTPurl} | {FTPurl} | {FILEurl}

The http path is optional ({HTTPpath}?); the ftp path is not. The ftp rule reuses the file rule's path production, which is right for file URLs only. Even the ftp fragment can only follow a path.

What the scanner does instead

When FTPurl cannot match, the scanner takes the longest match of its other rules, piece by piece: ftp is an ordinary word (<ALPHANUM>) and :// gives no token. What follows is typed by whichever rule matches it. A host that is a domain with a listed TLD or an IP address ({URIhostStrict}, jflex:157) followed by a port or a fragment is a scheme-less URL, {HTTPurlNoScheme} (jflex:168-174, returned as <URL> at :273), e.g. mirror.example.org:2121. Such a host on its own is a <URL> when a non-word character follows it (jflex:276), and at the end of the text when it is a domain (:283-290) or a bracketed IPv6 address ([::1]). Otherwise it is a word (localhost) or a number (an IPv4 address at the end of the text). With a user name and no password the e-mail rule wins (jflex:297): / is a legal e-mail local-part character (jflex:188), so //anonymous@ftp.example.org is an <EMAIL>; with a password, user:secret is a word.

inputstock mainwith the fix
ftp://ftp.example.com/ (control)[ftp://ftp.example.com/ <URL>]same
http://localhost (control)[http://localhost <URL>]same
ftp://ftp.example.com[ftp <ALPHANUM>] [ftp.example.com <URL>]one <URL>
ftp://mirror.example.org:2121[ftp <ALPHANUM>] [mirror.example.org:2121 <URL>]one <URL>
ftp://localhost[ftp <ALPHANUM>] [localhost <ALPHANUM>]one <URL>
ftp://192.0.2.10 (end of text)[ftp <ALPHANUM>] [192.0.2.10 <NUM>]one <URL>
see ftp://192.0.2.10 now... [ftp <ALPHANUM>] [192.0.2.10 <URL>] ...one <URL>
ftp://anonymous@ftp.example.org[ftp <ALPHANUM>] [//anonymous@ftp.example.org <EMAIL>]one <URL>
ftp://user:secret@ftp.example.com:21[ftp <ALPHANUM>] [user:secret <ALPHANUM>] [ftp.example.com:21 <URL>]one <URL>
ftp://ftp.example.com#top[ftp <ALPHANUM>] [ftp.example.com#top <URL>]one <URL>
ftp://[::1][ftp <ALPHANUM>] [[::1] <URL>]one <URL>
file://host (control: file URLs need the /)[file <ALPHANUM>] [host <ALPHANUM>]same
UAX29URLEmailAnalyzer: FTP://ftp.example.org[ftp <ALPHANUM>] [ftp.example.org <URL>][ftp://ftp.example.org <URL>]
Exposure and likelihood294 words
  • Who. Default settings and the public API are enough; no special configuration is needed. It is opt-in: the default StandardAnalyzer has no URL rules. Downstream servers expose the tokenizer too: Solr through the factory (its Reference Guide lists "file://, http(s)://, and ftp:// URLs" among the forms kept as single tokens), and Elasticsearch's uax_url_email tokenizer (its maintainers sent a report about it to Lucene: "this tokenizer belongs to Lucene", quoted in #9092).
  • What is lost. One document mirror at ftp://ftp.example.org today, indexed with UAX29URLEmailAnalyzer: TermQuery(body:ftp://ftp.example.org) finds 0 of 1 (1 of 1 for the same text with a trailing /, and 1 of 1 with the fix). Anything that keeps <URL> tokens (link extraction, URL facets, a type filter) gets the bare host or nothing (ftp://localhost, ftp://192.0.2.10 at the end of the text); an e-mail extractor collects //anonymous@ftp.example.org as an address.
  • What still works. A query analyzed by the same analyzer is split the same way: the phrase query for ftp://ftp.example.org built by QueryBuilder finds 1 of 1. Word search on the host matches.
  • Long URLs. Every tested input is short. As for any token of this tokenizer, a URL longer than the maximum token length (default 255, StandardAnalyzer.java:37; setMaxTokenLength) never comes out as one token, http or ftp, with or without the fix: the javadoc says such tokens "will be chopped up at this token length and emitted as multiple tokens" (UAX29URLEmailTokenizer.java:91-95). No long URL was run.
  • How likely. Ftp links are uncommon in today's text and most carry a path (all 154 ftp URLs in each of the two upstream URL fixtures do); nobody has reported it since 3.1.0 (2011). Registry: likelihood rare, priority low.
  • Workaround. Write ftp links with a trailing /.

Minimal repro

The test shows that UAX29URLEmailTokenizer does not keep the path-less ftp URL ftp://localhost as one <URL> token (RFC 1738 3.2.2 makes the ftp path optional).

TestFtpUrlWithoutPath.java (JUnit test)31 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.analysis.email;

import java.io.StringReader;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.tests.analysis.BaseTokenStreamTestCase;

public class TestFtpUrlWithoutPath extends BaseTokenStreamTestCase {

  public void testFtpUrlWithoutPath() throws Exception {
    Tokenizer t = new UAX29URLEmailTokenizer();
    t.setReader(new StringReader("ftp://localhost"));
    // RFC 1738 3.2.2 lets the ftp path be omitted: expected one <URL> token, actual "ftp" + "localhost"
    assertTokenStreamContents(t, new String[] {"ftp://localhost"}, new String[] {"<URL>"});
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-738/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble   (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/analysis/common/build/libs/lucene-analysis-common-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar   # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar   # JUnit Platform console launcher (Maven Central)

javac -d classes -cp "$CP" $(find src -name '*.java')

# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
     -cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
     execute --select-class org.apache.lucene.analysis.email.TestFtpUrlWithoutPath
Expected vs actual
Expected one token ftp://localhost of type <URL>; actual ftp + localhost: term 0 expected:<ftp[://localhost]> but was:<ftp[]> (-ea and -da).

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

More files, environment, full output38 words

Environment

luceneLucene main e1faeba08bde (2026-11-02)
jdk25.0.3+9-LTS

Observed output

# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
    => org.junit.ComparisonFailure: term 0 expected:<ftp[://localhost]> but was:<ftp[]>
       org.junit.Assert.assertEquals(Assert.java:117)
       org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:262)
       org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:513)
       org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:598)
       org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:622)
       org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:694)
       org.apache.lucene.analysis.email.TestFtpUrlWithoutPath.testFtpUrlWithoutPath(TestFtpUrlWithoutPath.java:29)

Why it looks unintended

  • The javadoc promises URL tokenization "according to the relevant RFCs"; the cited RFC 1738 says "The whole url-path may be omitted, including the "/" delimiting it".
  • The http rule beside it makes the path optional; only the file rule requires it, as RFC 1738 does.
  • Nothing records the restriction, and similar URL mis-typing here was fixed as a bug (LUCENE-8278).
The full argument332 words
  • Contract. Class javadoc (UAX29URLEmailTokenizer.java:31-33), package javadoc (package-info.java:21-25) and the 3.1.0 CHANGES entry for LUCENE-2167 (CHANGES.txt:17763-17774): URLs and e-mail addresses are tokenized "according to the relevant RFCs". The grammar header lists RFC 1738 and RFC 3986 (jflex:113-122).
  • The RFCs. RFC 1738 section 3.1: "/<url-path>" "may be excluded"; section 3.2.2: "The whole url-path may be omitted, including the "/" delimiting it"; section 5: ftpurl = "ftp://" login [ "/" fpath [ ";type=" ftptype ]]. RFC 3986 section 3.3: with an authority, "the path component must either be empty or begin with a slash".
  • The author's own description (java-user@): in June 2010, of the NewStandardTokenizer in the then-current LUCENE-2167 patch, whose URL grammar was later moved into UAX29URLEmailTokenizer: "It keeps (HTTP(S), FTP, and FILE) URLs together as single tokens, and e-mails too, in accordance with the relevant IETF RFCs"; in 2012, of UAX29URLEmailAnalyzer: "Full support for URLs with the file:, ftp:, and http/s: protocols".
  • Tests. No test pins either behaviour: all 154 ftp URLs in each URL fixture have a path. Deliberate deviations are written down (jflex:172, jflex:275 LUCENE-5391, the mailto: TODO in TestUAX29URLEmailTokenizer); this one is not.
  • History. The rule is in the commit that wrote the URL grammar (3c26a9167cc, LUCENE-2167, 2010) and never changed. The issue mentions ftp once: "there is no testing yet for the file:, https:, or ftp: schemes". The author typed both example.com/ and example.com as <URL> because "they effectively refer to the same resource (when interpreted in the context of the HTTP URI scheme)"; they said it of http only, and applying it to ftp://host and ftp://host/ is our reading, which RFC 1738 section 3.2.2 supports. The history shows that no reason for the mandatory ftp path was recorded, not that it was an accident.
  • Precedent. LUCENE-8278 (7.4.0 Bug Fixes, CHANGES.txt:7485), LUCENE-5391 (CHANGES.txt:12858) and GITHUB#12561 (11.0.0 Bug Fixes, CHANGES.txt:307) fixed URL and e-mail mis-typing in this grammar as bugs.
The devil's-advocate check205 words

The strongest "intended" case (moderate per the cross-model reviews, moderate-to-weak per the independent reproduction; the intended-behaviour check stated it without a rating and rejected it): the tokenizer is a heuristic recognizer, selective elsewhere (four schemes, TLD-gated scheme-less URLs, mailto: dropped); "according to the relevant RFCs" can be read as soundness, not completeness; the author wanted to "only accept strictly correct URLs" (answering a concern about false positives from URL rules and URLs without http://); the rule is 16 years old and every fixture has a path; the host often survives as a <URL> token.

Why it fails: ftp is one of the four supported schemes and the cited RFC says its path may be omitted, so ftp://host is RFC-valid; the "strictly correct" remark is consistent with the fix, though the thread does not decide this case. The author followed each scheme's RFC 1738 BNF elsewhere (optional http path, required file path). The output is not a clean refusal but wrong tokens and types (<NUM>, <EMAIL>), so even the soundness reading is not met. Age and fixtures are not intent (they show only that no reason for the restriction was recorded), and compatibility goes to severity. Verdict: a defect (adjudication: confirmed, high).

Suggested fix

Recommended Make the ftp path optional in the grammar and regenerate the scanner

Make the path optional in the ftp rule, ({FTPorFILEpath} {FTPtype}?)? at UAX29URLEmailTokenizerImpl.jflex:180, and regenerate the scanner. The new test, the email suites (96 tests) and the whole module (1,915 tests) pass. It changes the tokens of path-less ftp URLs, so affected fields need a reindex.

 FTPscheme = [fF][tT][pP] "://"
-FTPurl = {FTPscheme} {URIauthorityLoose} {FTPorFILEpath} {FTPtype}? {URIfragment}?
+FTPurl = {FTPscheme} {URIauthorityLoose} ({FTPorFILEpath} {FTPtype}?)? {URIfragment}?
All options and trade-offs3 options ยท 411 words

One option was built and validated with gradle; the others are noted with their costs and were not run.

Make the ftp path optional in the grammar and regenerate the scanner (validated by the checks) (recommended)

One grammar line (jflex:180), the same change as the finder's; ;type= stays tied to a path, as in RFC 1738. Then ./gradlew :lucene:analysis:common:regenerateUAX29URLEmailTokenizer -x :lucene:analysis:common:regenerateTlds rewrites UAX29URLEmailTokenizerImpl.java and its checksum file.

 FTPscheme = [fF][tT][pP] "://"
-FTPurl = {FTPscheme} {URIauthorityLoose} {FTPorFILEpath} {FTPtype}? {URIfragment}?
+FTPurl = {FTPscheme} {URIauthorityLoose} ({FTPorFILEpath} {FTPtype}?)? {URIfragment}?

Trade-offs

  • Changes the tokens of every path-less ftp URL: a CHANGES entry with a reindex note; main is the natural target.
  • The regenerated scanner (1.85 MB) differs in about 9,600 lines; the regeneration needs a 12 GB heap and took 4 min 23 s here. Without -x regenerateTlds it first downloads the current IANA TLD list and may rewrite ASCIITLD.jflex, an unrelated change.
  • Edge cases now match the http rule: see ftp://ftp.example.com. and (ftp://ftp.example.com), next keep the URL whole without the punctuation; ftp:// and ftp://-x are unchanged; file://host stays split. No tokenization benchmark was run.
  • The sibling bug LFV-739 changes other lines of the same grammar; one regeneration covers both, but whether the two changes compose was not checked.

What the checks validated

  • Scratch worktree of a9779ef3c193, gradle, JDK 25: stock + the new test fails 5 of 6 with assertions on and off; with the fix it passes 6 of 6.
  • --tests 'org.apache.lucene.analysis.email.*' (3 upstream suites + the new one): 96 tests, 1 skipped (@Nightly), 0 failures, assertions on and off. The whole :lucene:analysis:common:test: 1,915 tests, 18 skipped, 0 failures, assertions on. checkGoogleJavaFormat and regenerateUAX29URLEmailTokenizerChecksumCheck pass.
  • The regenerated scanner equals the finder's independent JFlex 1.9.1 output except for its header comment. With it shadowed: the finder's test 4 of 4, the independent reproduction's 11 of 11, the cross-model reviewer's 4 of 4, and the hunt's translation-validation harness loses the {ftp, no path} failure set without gaining any.

Document that ftp URLs need a path

Say so in the class and package javadoc. Not run.

Trade-offs

Changes no tokens, but contradicts RFC 1738 3.2.2, keeps the http/ftp asymmetry, and leaves the <NUM> and <EMAIL> fallbacks.

Application-side workaround: add the slash before tokenizing

A char filter (for example a PatternReplaceCharFilter) that appends / to path-less ftp URLs. Not run.

Trade-offs

No Lucene change, but it alters the indexed text of those URLs and every application must do it itself.

Downstream note

Applications that pull links out of text by keeping <URL> tokens get only the bare host for ftp://ftp.example.com and nothing at all for ftp://localhost.

How it was found

A bounded-exhaustive comparison of the stock tokenizer with an independent reference written from RFC 1738 and RFC 3986, over a finite matrix of generated URLs in several contexts: every path-less ftp case in it failed. Reading the ftp rule then gave the cause.

The method in full157 words

Method: bounded-exhaustive translation validation (the hunt's semi-formal tier), a hunt unit (classic, Wikipedia and URL/e-mail tokenizers against their documented rules), 2026-10-27.

  • An AI finder wrote an independent reference from the RFC 1738 / RFC 3986 grammar that the tokenizer cites and generated URLs from 5 schemes x 5 userinfo forms x 10 hosts x 4 ports x 19 paths x 7 queries x 5 fragments, each in 6 surrounding contexts. The completeness oracle: a valid URL between neutral delimiters comes out as exactly one <URL> token.
  • Every path-less ftp case in this finite matrix failed, so {ftp, no path} came out as a minimal always-failing feature set. The same run flagged other feature sets that are separate findings, among them the sibling path-character bug (LFV-739).
  • Reading the FTPurl rule gave the root cause; the finder then reduced it to a public-API JUnit. Against the fixed scanner the harness loses {ftp, no path} and gains no failing set.

Prior reports, history and checks

Prior reports

No prior report found (2026-10-27): 138 tool queries over GitHub, commits, mailing lists, JIRA and the web, 52 manual GitHub searches, all 410 open PRs, and 335 list mails grepped for "ftp". The nearest issues concern other URL rules (#9092, #9325).

Searches4 searches

Best-effort, read-only search; verdict new. No report, issue, PR, list thread, forum post or commit describes this bug, and no open PR touches the fix site.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene, downstream, all of GitHub)2026-10-27a scripted prior-report search (40 issue/PR, 3 commit, 2 downstream queries) plus the independent reproduction and the intended-behaviour check runs (63 queries); 52 manual searches, e.g. UAX29URLEmailTokenizer ftp, uax_url_email ftp, FTPurl, FTPorFILEsegment; the discussions of 62 hits grepped for ftpno report; related: #9092 (scheme-less URLs), #9325 (LUCENE-8278), #6454, #4953, #12561 / #12583, #7409, #3243 (origin of the rule)
Open PRs and code copies2026-10-27the changed files of all 410 open apache/lucene PRs; 2 code searches (99 non-Lucene copies of the grammar fetched)only #16911 lists the grammar file (it deletes all of branch_10x); every copy has the same FTPurl line
Mailing lists and JIRA2026-10-2720 tool queries plus 64 direct searches over 8 lists (java-user@, dev@, issues@, general@, Solr lists), the bodies of all 335 returned mails grepped for ftp; 4 + 9 JIRA searches (LUCENE, SOLR), all LUCENE-2167 commentsno report; the grammar's author describing ftp URLs as kept whole (2010, 2012)
Web2026-10-276 tool and 6 manual searches, 11 page fetches (forums, Solr docs)no report; the Solr Reference Guide lists ftp:// URLs among single-token forms

History

Introduced with the URL grammar itself by LUCENE-2167 (3c26a9167cc, 2010-09-28), first released in 3.1.0, never changed. A version-independent probe fails on all 138 releases from 3.1.0 to 10.5.2 and on main; 3.0.3 has no such tokenizer.

Versions tested6 rows

Introduced by 3c26a9167cc (trunk, LUCENE-2167 "Implement StandardTokenizer with the UAX#29 Standard", #3243, Steven Rowe, 2010-09-28; svn era, no PR); the branch_3x copy ec0e73cd04f (2010-09-29) shipped first. LUCENE-2763 (#3837, 2010-12-07) moved the grammar unchanged into UAX29URLEmailTokenizer before any release. The FTPurl line is byte-identical in all 13 commits that add, copy, move or delete it (the trunk commit that introduced it, its branch_3x copy and 11 later ones), and unchanged on main, branch_10x and branch_10_6 at upstream main a95c754ee003 (2026-10-26). In 4.7.0-7.3.1 a bare ftp://ftp.example.com at the end of the input also leaves the host typed <ALPHANUM> (LUCENE-8278, fixed in 7.4.0); no version emits one <URL> token. The release sweep runs 3 path-less inputs (ftp://ftp.example.com, ftp://localhost, the first inside a sentence) and 3 controls; the IPv4, userinfo and port forms ran on main, 10.5.2 and 9.12.3 only, and the IPv6, password and fragment forms on main only.

Versions and branches executed, with results
Version or commitVerdictResult
main a9779ef3c193 affectedthe brief's test 5 of 6 fail (gradle and the standalone runner, assertions on and off)
10.5.2, 9.12.3 affectedthe brief's test 5 of 6 fail; the finder's 2 of 4; the independent reproduction's 6 of 11
3.1.0 to 10.5.2 (all 138 releases) affectedthe version-independent probe: 3 of 3 bug cases fail (ftp://ftp.example.com, ftp://localhost, the first in a sentence), 3 of 3 controls pass on each
4.7.0 to 7.3.1 variantat the end of the input the host part is typed <ALPHANUM> instead of <URL> (LUCENE-8278)
3.0.3 API absentno UAX29URLEmailTokenizer; StandardTokenizer has no <URL> type
main + fix not affectedthe brief's test 6 of 6; email suites 96 tests and the module's 1,915 tests, 0 failed

Independent checks

An independent reproducer, a devil's advocate, an adjudication and reviews by another model family (which ran their own tests) all found it real; the adjudication confirmed it with high confidence. A registry re-run, an audit and the brief's gradle runs reproduced it.

Checks and verdicts317 words

Each check ran its own test or probe on stock jars; the cross-model reviews ran on main ea57360912ed, whose lucene/analysis sources are identical to a9779ef3c193.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidatePublic-API JUnit fails 2 of 4 on main (-ea, -da), 10.5.2 and 9.12.3; the fixed scanner passes it and the 79 upstream tokenizer and analyzer tests.
cross-model review, refute lens real (high)An independent reviewer from another model family (OpenAI GPT-5.6) reproduced both cases with its own test, -ea and -da, repeated seeds; the fix passes it and the existing suite (79 of 80, 1 nightly aborted, 0 failures).
Independent reproducer realOwn 11-test JUnit: 6 of 11 fail on main, 10.5.2 and 9.12.3 (host, host in text, port, IPv4 <NUM>, userinfo <EMAIL>, analyzer); 5 controls pass; 11 of 11 with the fix.
Cross-model review, intended lens real (high)OpenAI GPT-5.6: own test 2 of 4 fail on main and 10.5.2, 4 of 4 with the fix; inputs in contract; intended argument moderate.
Devil's advocate real (not intended, high)All 9 path-less ftp forms split through tokenizer and analyzer; file://host correctly not a URL; added the <EMAIL> symptom, the RFC text and the LUCENE-2167 history.
adjudication confirmed (high)Re-ran the independent reproduction's test on stock main: 6 of 11 fail with -ea and -da, the 5 controls pass; duplicate of none.
registry re-run and audit keepFresh seeds on a clean checkout: 2 of 4 fail (-ea, -da), 4 of 4 with the fix; the audit's own token dump agrees and notes that same-analyzer phrase queries still match.
brief reproduced, fix validatedNew 6-test JUnit: 5 of 6 fail via gradle and the standalone runner; the fix regenerated with gradle passes it, the email suites and the whole module.

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

Finding record

LFV-738, snapshot of 2026-11-06

Registry title
UAX29URLEmailTokenizer does not emit an ftp URL with no path ('ftp://host') as one URL token, although its http rule and RFC 1738 make the path optional
Area
analysis / UAX29URLEmailTokenizer JFlex URL rules (FTPurl production; analysis-common)
Priority
Low, rank 518: An ftp URL with no path is a rare input, and the opt-in UAX29URLEmailTokenizer only loses the single URL token while the host stays searchable.
Impact class
silent wrong results
Impact note
Anyone who analyzes text with UAX29URLEmailTokenizer, UAX29URLEmailAnalyzer or the 'uax29UrlEmail' tokenizer factory (default settings, public API, no scale) and meets an ftp URL written with no path at all, not even a trailing '/' ('ftp://ftp.example.org', 'ftp://host:2121', 'ftp://localhost', 'ftp://192.0.2.10', 'ftp://anonymous@ftp.example.org'), gets no single <URL> token and no error: the scheme is split off as 'ftp' <ALPHANUM> and the rest comes out as <URL> (hosts with a listed TLD), <ALPHANUM> (localhost, dotless names), <NUM> (IPv4) or <EMAIL> (with userinfo), so exact 'ftp://host' terms, scheme-qualified matching and <URL>-type extraction miss it while word-level search on the host mostly still matches; -ea and -da behave the same, and a fix changes indexed tokens, so affected fields need reindexing after it.
Summary
You run text such as 'get it from ftp://ftp.example.com today' or 'ftp://localhost' through UAX29URLEmailTokenizer, whose javadoc says URLs are 'tokenized according to the relevant RFCs', and expect each ftp URL as one <URL> token, because RFC 1738 (section 3.2.2: 'The whole url-path may be omitted, including the "/" delimiting it') and RFC 3986 (an empty path after an authority) allow it, and the same tokenizer already keeps 'http://localhost' whole. Instead it emits 'ftp' as <ALPHANUM> followed by the rest on its own ('ftp.example.com' as <URL>, 'localhost' as <ALPHANUM>, '192.0.2.10' as <NUM>, '//anonymous@ftp.example.org' as <EMAIL>), because the grammar's FTPurl rule makes the '/'-led FTPorFILEpath mandatory while the http rule beside it makes its path optional. With a trailing slash ('ftp://ftp.example.com/') the URL is one token, and nothing reports an error.
Introduced
3c26a9167cc3a4f5d338e9b74d0af61d6de9d773 (LUCENE-2167 'Implement StandardTokenizer with the UAX#29 Standard', apache/lucene#3243; trunk svn r1002032 by sarowe, Subversion era, no pull request; branch_3x backport ec0e73cd04f940d6caf8308207577313e78a9009, svn r1002468, 2010-09-29, the copy that first shipped): it added FTPorFILEpath = "/" {FTPorFILEsegment} ... and FTPurl = {FTPscheme} {URIauthorityLoose} {FTPorFILEpath} {FTPtype}? {URIfragment}?, while HTTPurlFull already had {HTTPpath}?; LUCENE-2763 (apache/lucene#3837; trunk 2b9726ae8169, branch_3x 543849ca43fd, 2010-12-07) moved the URL and e-mail grammar unchanged into the new UAX29URLEmailTokenizer before any release. Not a regression: the FTPurl and FTPorFILEpath lines are byte-identical in all 13 commits that add or remove them (back-compat copies, moves, deletions) [read: git blame -w -C -C -M of main's jflex:176-185, git log --all -G '^FTPurl *=' on *.jflex, tag containment; exec: the history probe on every release from 3.1.0 to 10.5.2] (2010-09-28 (trunk; branch_3x backport 2010-09-29; first release 3.1.0, tag dated 2011-03-30))
First affected version
3.1.0 [exec] (the first release with UAX29URLEmailTokenizer; the follow-up's version-portable probe fails 3 of 3 bug cases with 3 of 3 controls passing on it; 3.0.3, the release before, has no UAX29URLEmailTokenizer class and its StandardTokenizer has no <URL> type [exec])
Affected versions
Every release that has UAX29URLEmailTokenizer, 3.1.0 through 10.5.2 and 10.6.0 (by reading), including 9.12.3 (latest 9.x), and main a9779ef3c193 (unreleased 11.0); never fixed. [exec] The follow-up's version-portable reflection probe (3 bug cases: 'ftp://ftp.example.com', 'ftp://localhost' and the host inside a sentence; 3 controls: an ftp URL with '/', 'http://localhost' and the sentence with '/') fails 3 of 3 with 3 of 3 controls passing on stock, SHA-1-checked jars of all 138 releases 3.1.0-3.6.2, 4.0.0-4.10.4, 5.0.0-5.5.5, 6.0.0-6.6.6, 7.0.0-7.7.3, 8.0.0-8.11.4, 9.0.0-9.12.3 and 10.0.0-10.5.2, and on main with -ea and -da; the finder's JUnit fails 2 of 4 (both controls pass) on 9.1.0, 9.12.3, 10.5.2 and main (-ea and -da; it does not compile on 9.0.0 and older because of the test-framework package change); the independent reproduction's test fails 6 of 11 (all 5 controls pass) on main (-ea, -da), 10.5.2 (-ea, -da) and 9.12.3 (-ea). One secondary detail varies: in 4.7.0-7.3.1 a bare 'ftp://ftp.example.com' at the end of the input leaves the host typed <ALPHANUM> rather than <URL> (LUCENE-8278, a different issue fixed in 7.4.0); no version emits one <URL> token. [read] The FTPurl line is byte-identical on main, branch_10x and branch_10_6 and at upstream main a95c754ee003 (2026-10-26). All runs with JDK 25 on aarch64.
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, 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 (searched 2026-10-27, read-only; the prior-report record, verdict new: scripted prior-report search A (40 GitHub issue/PR, 3 commit, 2 downstream-repository, 20 mailing-list over dev@, java-user@, issues@ and commits@, 4 JIRA-archive and 6 web queries; 236 distinct hits: 0 high, 24 medium, 212 low; all 24 medium hits read) plus the independent reproduction and the intended-behaviour check scripted prior-report searches (63 queries, re-read); 52 manual GitHub issue/PR searches over apache/lucene, Elasticsearch, OpenSearch, Solr, derived tokenizers and all of GitHub; the changed-file lists of all 410 open apache/lucene PRs; 2 GitHub code searches (the FTPurl line of 99 non-Lucene copies of the grammar checked: all unpatched); 64 direct searches over 8 mailing lists with the bodies of all 335 returned mails grepped for 'ftp'; 9 JIRA searches (LUCENE, SOLR) and the full comments of 12 issues, including all LUCENE-2167 comments; 6 web searches and 11 page fetches; the upstream history of the grammar file on 5 branches). No report, issue, PR, list thread, forum post or commit about this bug. Nearest items, different defects: #9092 (LUCENE-8044, open: scheme-less and relative URLs), #9325 (LUCENE-8278, fixed in 7.4: end-of-input scheme-less hosts typed <ALPHANUM>; precedent that URL mis-typing in this grammar is fixed as a bug), #6454 (LUCENE-5391), #4953 (LUCENE-3880), #12561 / #12583 (other grammar rules), #7409 (LUCENE-6348, maxTokenLength, not a problem), #3243 (LUCENE-2167, origin of the rule; its only ftp remark is a 2010 progress note that ftp was not yet tested). Supporting the expected behaviour: the grammar's author wrote on java-user@ (2010, 2012) that ftp URLs are kept whole according to the RFCs, and the Solr Reference Guide lists 'ftp:// URLs' among the forms preserved as single tokens without a path caveat. Not fixed upstream: the FTPurl line is unchanged on main, branch_10x and branch_10_6 at upstream main a95c754ee003, and no open PR touches it (the only open PR listing the grammar file, #16911, deletes every file on branch_10x). Update 2026-11-02: #16911 (open when searched) merged 2026-10-29 as 48cf3f515d6, which emptied branch_10x to a README tombstone (10.6 is the last 10.x minor; 10.x fixes go to branch_10_6); it fixes nothing here.
Lucene commit tested
main a9779ef3c193 (commit of 2026-10-26; tested 2026-10-28) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off; the 10.5.2 and 9.12.3 release jars (the brief's test); a version-independent probe on every release from 3.0.3 (no such class) and 3.1.0 to 10.5.2 and on main (2026-10-27). The grammar fix was regenerated with gradle (JFlex 1.9.1) and validated in the scratch worktree: the new test, the email suites with assertions on and off, the whole module's 1,915 tests with assertions on, checkGoogleJavaFormat and the regeneration checksum check.

Lucene and upstream sources

Finding record
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/email/UAX29URLEmailTokenizerImpl.jflex:113-122 (the RFC list, incl. RFC-1738 and RFC-3986), :165 (HTTPpath), :167 (HTTPurlFull with {HTTPpath}?), :176 (FTPorFILEsegment), :177 (FTPorFILEpath starts with '/'), :180 (FTPurl with mandatory {FTPorFILEpath}), :183 (FILEurl, which correctly requires the path), :185 (URL = {HTTPurl} | {FTPurl} | {FILEurl})
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/email/UAX29URLEmailTokenizer.java:31-33 ('URLs and email addresses are also tokenized according to the relevant RFCs'), package-info.java:21-25 (the same sentence) and UAX29URLEmailTokenizerFactory.java:40 (NAME = "uax29UrlEmail")
The finding in 30 seconds
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/email/UAX29URLEmailTokenizerImpl.jflex:113-122 (at a9779ef3c193) (RFC list), :162, :165-167 (http), :172 (comment), :176-180 (ftp), :182-183 (file), :185, :188, :273, :276, :283-290, :297
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/email/UAX29URLEmailTokenizer.java:31-33; (at a9779ef3c193) package-info.java:21-25; UAX29URLEmailAnalyzer.java:91; UAX29URLEmailTokenizerFactory.java:40
  • lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:136-142; (at a9779ef3c193) lucene/core/src/java/org/apache/lucene/analysis/standard/StandardTokenizer.java:67-77
Who is affected
  • lucene/analysis/common/src/test/org/apache/lucene/analysis/email/random.text.with.urls.txt (at a9779ef3c193) and urls.from.random.text.with.urls.txt (154 ftp URLs each, 0 without a path; counted 2026-10-28)
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/email/UAX29URLEmailTokenizer.java:89-110 (maxTokenLength, setMaxTokenLength javadoc); lucene/core/src/java/org/apache/lucene/analysis/standard/StandardAnalyzer.java:37 (DEFAULT_MAX_TOKEN_LENGTH = 255)
Why it looks unintended
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/email/UAX29URLEmailTokenizer.java:31-33; (at a9779ef3c193) package-info.java:21-25
  • lucene/CHANGES.txt:307 (at a9779ef3c193) (11.0.0 Bug Fixes, :268), :7485 (7.4.0 Bug Fixes, :7437), :12858 (4.7.0), :17763-17774 (3.1.0)
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/email/UAX29URLEmailTokenizerImpl.jflex:113-122, (at a9779ef3c193) :167, :172, :177, :180, :183, :275
  • lucene/analysis/common/src/test/org/apache/lucene/analysis/email/TestUAX29URLEmailTokenizer.java:588-632 (at a9779ef3c193) (testMailtoSchemeEmails, TODO :596), :633-677 (testURLs over the fixtures)
Suggested fix
  • lucene/core/build.gradle:202-231 (at a9779ef3c193) (regenerateUAX29URLEmailTokenizer, heapSize 12g, dependsOn regenerateTlds)

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 same evidence; see the diagram's own notes for its frames and checks.

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.