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.
- Tokenize
get it from ftp://ftp.example.com todaywithUAX29URLEmailTokenizer,UAX29URLEmailAnalyzeror theuax29UrlEmailfactory, default settings. - You expect 5 tokens, with
ftp://ftp.example.comas 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 keepshttp://localhostandftp://ftp.example.com/whole. - You get 6:
ftp<ALPHANUM>andftp.example.com<URL>. Other hosts fare worse:ftp://localhostgives two words,ftp://192.0.2.10a<NUM>, andftp://anonymous@ftp.example.orgthe<EMAIL>//anonymous@ftp.example.org. - 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. - No error. An exact
TermQueryforftp://ftp.example.orgfinds 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.
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.
UAX29URLEmailTokenizer: Unicode word breaking (UAX#29) plus URL and e-mail recognition, generated by JFlex fromUAX29URLEmailTokenizerImpl.jflex. Used byUAX29URLEmailAnalyzerand theuax29UrlEmailfactory. Not the default:IndexWriterConfigdefaults toStandardAnalyzer, 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).
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.
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.
| input | stock main | with 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
StandardAnalyzerhas 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'suax_url_emailtokenizer (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 withUAX29URLEmailAnalyzer: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.10at the end of the text); an e-mail extractor collects//anonymous@ftp.example.orgas an address. - What still works. A query analyzed by the same analyzer is split the same way: the phrase query for
ftp://ftp.example.orgbuilt byQueryBuilderfinds 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
/.
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).
/*
* 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>"});
}
}
# 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://localhostof type<URL>; actualftp+localhost:term 0 expected:<ftp[://localhost]> but was:<ftp[]>(-ea and -da).
More files, environment, full output38 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# 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)- 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
NewStandardTokenizerin the then-current LUCENE-2167 patch, whose URL grammar was later moved intoUAX29URLEmailTokenizer: "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, ofUAX29URLEmailAnalyzer: "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:275LUCENE-5391, themailto:TODO inTestUAX29URLEmailTokenizer); 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 bothexample.com/andexample.comas<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 toftp://hostandftp://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).
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 regenerateTldsit first downloads the current IANA TLD list and may rewriteASCIITLD.jflex, an unrelated change. - Edge cases now match the http rule:
see ftp://ftp.example.com.and(ftp://ftp.example.com), nextkeep the URL whole without the punctuation;ftp://andftp://-xare unchanged;file://hoststays 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.checkGoogleJavaFormatandregenerateUAX29URLEmailTokenizerChecksumCheckpass.- 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.
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.
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.
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.
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
FTPurlrule 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.
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.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene, downstream, all of GitHub) | 2026-10-27 | a 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 ftp | no report; related: #9092 (scheme-less URLs), #9325 (LUCENE-8278), #6454, #4953, #12561 / #12583, #7409, #3243 (origin of the rule) |
| Open PRs and code copies | 2026-10-27 | the 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 JIRA | 2026-10-27 | 20 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 comments | no report; the grammar's author describing ftp URLs as kept whole (2010, 2012) |
| Web | 2026-10-27 | 6 tool and 6 manual searches, 11 page fetches (forums, Solr docs) | no report; the Solr Reference Guide lists ftp:// URLs among single-token forms |
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.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | the brief's test 5 of 6 fail (gradle and the standalone runner, assertions on and off) |
| 10.5.2, 9.12.3 | affected | the 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) | affected | the 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 | variant | at the end of the input the host part is typed <ALPHANUM> instead of <URL> (LUCENE-8278) |
| 3.0.3 | API absent | no UAX29URLEmailTokenizer; StandardTokenizer has no <URL> type |
| main + fix | not affected | the brief's test 6 of 6; email suites 96 tests and the module's 1,915 tests, 0 failed |
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.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Public-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 | real | Own 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 | keep | Fresh 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 validated | New 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. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 505 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,470 words
- 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,checkGoogleJavaFormatand the regeneration checksum check.
- 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, :297lucene/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:40lucene/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-25lucene/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, :275lucene/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)
- TestFtpUrlWithoutPath.java (JUnit test):
repro/LFV-738/src/org/apache/lucene/analysis/email/TestFtpUrlWithoutPath.java(31 lines)
Made in parallel with this doc from the same evidence; see the diagram's own notes for its frames and checks.
assets/LFV-738/bug.png(505 KB)
