Hi, I apologize for the wall of text that follows. In this age of agents we had Claude investigate upgrading one of our apps from 6.2 to 7.1.2.184-RC and to 8.0.0.294-RC (the newest release candidates at the time of writing). These are issues that were identified for us. I see on Jira it says please post to dev.lucee.org first before creating issues, and so I am doing that here. We currently build our servers in dev and on docker by starting with the lucee zero jar, then adding the lex extensions we need from the local file system - for Lucee 7+ we are referencing them in .CFConfig.json directly to get them loaded in on startup, though we tried several approaches to loading them. What follows here is the write up from Claude, given my limited Java knowledge I haven’t confirmed all of this on my own, particularly the fixes for the issues:
1. Warm-up runs onBuild before the extensions are installed (quick reload calls onStart)
Versions: 7.1.2.184-RC; same code on 8.0.
Summary. On a fresh server home whose .CFConfig.json lists extensions, LUCEE_ENABLE_WARMUP=true
calls Server.cfc’s onBuild from inside the last extension install, before the extensions are
registered, and then exits the JVM. The warm-up has to be run twice to get a usable image.
Repro.
-
Empty server home. WEB-INF/lucee-server/context/.CFConfig.json:
{
"extensions": [
{ "id": "37C61C0A-5D7E-4256-8572639BE0CF5838", "version": "3.1.0.1" }
]
}
(Guard / ESAPI, but any extension does.)
-
WEB-INF/lucee-server/context/context/Server.cfc:
component {
function onBuild() {
systemOutput( "extensions installed: " & extensionList().recordCount, true );
systemOutput( encodeForHTML( "<x>" ), true ); // a function the extension provides
}
}
-
LUCEE_ENABLE_WARMUP=true java ... runwar.Start ... (or Tomcat).
Observed. The deploy log shows the extension installing, then onBuild runs and prints
extensions installed: 0; the encodeForHTML line fails to compile (function not found).
Lucee logs “Lucee warmup completed” and exits 0. The version and required-extension stamps
in the server home are not written. A second warm-up start finds the extension installed, writes
the stamps, does the full load and runs onBuild correctly.
Cause. CFMLEngineImpl sets quick = true for the whole install phase (CFMLEngineImpl.java:452,
cleared at :595). DeployHandler.deployExtensions reloads the config only on the last extension
(reload = i + 1 == eds.length). ConfigFactoryImpl.reloadInstance loads with essentialOnly = quick,
which skips _loadExtensionBundles, and then calls onStart(configServer, true) unconditionally
(ConfigFactoryImpl.java:390-391). The fresh-start path guards the same call with if (!essentialOnly)
(ConfigFactoryImpl.java:345-347). With the warm-up flag, onStart runs onBuild and calls
System.exit(0), before deployExtensions’ finally resets the tag and function libraries and before
the full load.
Suggested fix. Skip onStart in reloadInstance when quick is set (mirror the !essentialOnly
guard), so the hook runs once, after the full load. Then a single warm-up installs the extensions and
runs onBuild with them available, which is what the warm-up docs describe.
2. Warm-up exits 0 when onBuild throws or is killed by the timeout
Versions: 7.1.2.184-RC; same code on 8.0.
Repro A (exception).
component {
function onBuild() {
throw( "build step failed" );
}
}
LUCEE_ENABLE_WARMUP=true java ...; echo $? prints 0. The exception is logged at ERROR
(LDEV-6472 added the logging) but the build succeeds.
Repro B (timeout). With requestTimeout at its default (50 s) or any value:
component {
function onBuild() {
sleep( 120000 ); // e.g. a Maven download on a slow mirror
}
}
After 50 s the log shows killing on-start at INFO, then “Lucee warmup completed”, exit 0.
Cause. CFMLEngineImpl.onStart (:1868-1878) calls onStartCall and then System.exit(0)
regardless of outcome. OnStart.run catches every exception (:1997-2003). onStartCall joins the
thread with config.getRequestTimeout() and stops it if still alive (:1907-1925); nothing inside
onBuild can extend that, because the join is in the parent thread, so setting requestTimeout
has no effect.
Suggested fix. Exit non-zero (and say so in the log) when onBuild threw or was killed. Either
do not apply the request timeout to the warm-up thread, or give it its own setting
(LUCEE_WARMUP_TIMEOUT), since a build step that downloads libraries is not a request.
Workaround we use. onBuild writes a marker file as its last statement and the shell script
fails without it.
3. An extension resource that does not exist is silently replaced by a download
Versions: 7.1.2.184-RC.
Repro. .CFConfig.json:
{
"extensions": [
{
"id": "671B01B8-B3B3-42B9-AC055A356BED5281",
"version": "42.7.11",
"resource": "/opt/vendored/postgresql-jdbc-extension-42.7.11.lex"
}
]
}
with the file absent (typo, wrong mount, placeholder not substituted). Start Lucee.
Observed. The deploy log says the extension was installed “from remote extension provider”
(or from Maven Central as org.lucee:postgresql-jdbc-extension). The installed set matches the
config, so nothing reports that the vendored file was never used. In an air-gapped or
downloads-blocked environment the install fails with a message about the network, not about the
missing file.
Cause. RHExtension.toExtensionDefinition (:1480-1482) attaches the resource only if
res != null && res.isFile(); otherwise the definition falls through to the id/version providers.
Related. Only ${VAR}, {env:VAR}, {system:prop} and {secret:...} are substituted in
resource; {lucee-server} and {web-root} are not, although they work in mappings. A
resource of {lucee-server}/deploy/foo.lex therefore hits this same fallback.
Suggested fix. When an entry declares resource (or path/url) and it does not resolve,
fail the install, or at minimum log at ERROR with the path that was tried. Support the
{lucee-server} placeholder in resource.
4. Lucee’s Maven extension provider rejects the version of its own MSSQL extension
Versions: 7.1.2.184-RC; same parser on 8.0.
Repro. .CFConfig.json:
{ "extensions": [ { "id": "99A4EF8D-F2FD-40C8-8FB8C2E67A4EEEB6", "version": "13.4.0.jre11" } ] }
Observed. Log: Given version [13.4.0.jre11] is invalid; the Maven provider gives up and Lucee
falls back to extension.lucee.org. org.lucee:mssql-jdbc-extension:13.4.0.jre11 exists on Maven
Central, so Lucee publishes a version its own provider cannot fetch.
Cause. lucee.runtime.config.maven.Version.parseVersion: with four dot-separated parts the
fourth must be an integer (build = Caster.toInteger(hp[0], null) in the default branch), and a
null build makes the version invalid. 13.4.0.jre11 has a non-numeric fourth part with no -.
Suggested fix. Treat a non-numeric trailing segment as the qualifier (13.4.0 + jre11), or
publish the extension under a Maven-parseable version. 13.4.0.5-RC on maven.lucee-services.com is
the same driver but another host and an RC label, so it is not a substitute.
5. The Maven resolver downloads every version in a dependency tree before picking one
Versions: 7.1.2.184-RC; same code on 8.0.
Summary. When Lucee loads a Maven coordinate (an extension’s maven="g:a:v" function or tag
attribute, a jdbc/cache manifest entry, this.javaSettings, mavenLoad()), it walks the full
POM tree with no version mediation, fetches the POM and the jar of every node, and only then
keeps the highest version per groupId:artifactId for the classpath. The losing versions are
downloaded and never used. With the download policy set to error, a missing losing version fails
the load even though the version that would be used is present.
Repro. The PDF extension 3.0.0.2 ships commons-io 2.19.0, commons-logging 1.3.5 and no
xml-apis. Its tree also references commons-io 2.17.0 (via xmlgraphics-commons),
commons-logging 1.3.0 and xml-apis 1.4.01.
- Fresh server home, install PDF 3.0.0.2 from its
.lex (seeds 38 jars into mvn/), no ~/.m2.
- Start with
LUCEE_MAVEN_DOWNLOAD_POLICY_RUNTIME=error.
<cfdocument format="pdf">x</cfdocument>
Observed. Lucee is unable to resolve the Maven artifact [commons-io:commons-io:2.17.0] (type: jar) because Maven downloads are blocked by policy, although commons-io 2.19.0 is in mvn/ and is the
version the classpath would receive. With downloads allowed, mvn/ ends up holding both versions
of each of the three libraries; mavenExport() lists them all.
Cause. POM.getDependencies (POM.java:600-640) adds a tree node per groupId:artifactId:version
and reads each node’s POM (initXML → MavenUtil.download(..., "pom")). POM.getJarPOMs
(:743-765) then calls getArtifact() on every node, which downloads the jar
(downloadArtifact, :399-412, rethrows the IOException for jar packaging). Only afterwards does
JavaSettingsImpl.getAllResources (JavaSettingsImpl.java:221-258) reduce the list to the highest
version per groupId:artifactId with OSGiUtil.compare.
The walk itself needs only POMs: a node’s children come from reader.getDependencies() on the
downloaded .pom, and the constructor’s initXMLAsync prefetches the POM alone (downloadAsync,
the one helper that could fetch a jar early, has no callers). So when the jar loop starts, the
full tree with every version is already known, and the losing jars are fetched after Lucee had
what it needed to know they would lose.
Suggested fix. Mediate between getAllDependencies() and the jar loop in getJarPOMs: pick
the winning version per groupId:artifactId, then download only those jars. The walk does not
have to change; the losing versions’ POMs are still read, as Maven does.
That shrinks what an image has to carry, makes mavenExport() reflect the real classpath, and lets
a .lex that ships its libraries work offline without extra downloads. Lower-cost alternative:
when a jar is missing and the policy blocks downloads, skip it if a higher version of the same
groupId:artifactId is already present.
Side note. Lucee picks the highest version rather than Maven’s nearest; that is a documented
difference worth stating in the Maven recipe, since the jar an extension ships may lose to a newer
one pulled in transitively.
6. (8.0) Server-level nullSupport is ignored where there is no page context
Versions: 8.0.0.294-RC. 7.1.2 passes.
Repro. .CFConfig.json has "nullSupport": true. No Application.cfc sets this.nullSupport.
// Consumer.cfc
component {
function init( string responseFormat = null ) {
variables.responseFormat = responseFormat; // unscoped read of a null argument
return this;
}
}
new Consumer(); // argument not passed
Observed. variable [RESPONSEFORMAT] doesn't exist. arguments.responseFormat, isNull( responseFormat )
and a null local variable all behave correctly. Adding this.nullSupport = true to any
Application.cfc makes it pass, which shows the server setting is the one being ignored.
Cause. LDEV-6276 (“remove dependency on ThreadLocal”, 8.0 only). NullSupportHelper.full()
called without a page context now returns false unless the static
ModernApplicationContext.hasCustomNullSupportSetting is set by some Application.cfc
(NullSupportHelper.java:33, ModernApplicationContext.java:288, 2109); 7.1 looked up the current
page context’s config. StructImpl.get(key, default) and the arguments scope’s getFunctionArgument
(including the new array storage from LDEV-5865) call it that way, so a null argument counts as
missing. LUCEE_SCOPE_ARGUMENTS_ARRAY=false does not help, because the other path calls the same
StructImpl.get.
Suggested fix. Fall back to the server config (ConfigServer.getFullNullSupport()) when there
is no page context and no application has set the flag.
7. (8.0) dotNotationUpperCase in .CFConfig.json is silently ignored
Versions: 8.0.0.294-RC. 7.1.2 honours it.
Repro. .CFConfig.json has "dotNotationUpperCase": false.
writeOutput( serializeJSON( { abcKey: 1 } ) );
7.1: {"abcKey":1}. 8.0: {"ABCKEY":1}. Replacing the key with "preserveCase": true restores it
on both versions.
Cause. 8.0 reads only preserveCase (and lucee.preserve.case / LUCEE_PRESERVE_CASE);
dotNotationUpperCase survives only in the code that migrates it out of the old compiler
section. No warning is logged.
Why it matters. This changes the case of every struct key in JSON responses, which breaks
front ends silently. The docs still name dotNotationUpperCase as the .CFConfig.json key
(processingdirective reference, preserveCase attribute page), and the 8.0 breaking-changes
page does not list it.
Suggested fix. Keep honouring dotNotationUpperCase as the inverse of preserveCase (or log a
warning naming the replacement), and add it to the 8.0 breaking-changes page and the upgrade
checklist.
8. (7.0+) A struct literal key named class, java, cfml or cfc followed by : and [ fails to compile
Versions: 7.1.2.184-RC and 8.0.0.294-RC. 6.2 compiles all of these.
Repro.
s = { class: [ 1, 2 ] }; // Invalid Syntax Closing []] not found
s = { Class: [ 0, 0 ].slice( 1, 1 ) }; // same
These work:
s = { "class": [ 1, 2 ] };
s = { class = [ 1, 2 ] };
s = { class: ( [ 1, 2 ] ) };
s = { class: 1 };
s = { class: "btn" };
Cause. Since commit b5363a88c (LDEV-3335, December 2025) the expression parser reads an
unquoted class, java, cfml or cfc directly followed by : as a type prefix, as in
new java:Foo(), and consumes the colon. In a struct literal, the [ that follows is then parsed
as bracket access on the prefix.
Why it matters. class is a very common struct key in HTML-helper and view code. The 7.0 and
7.1 breaking-changes pages do not mention it, and the error message gives no hint.
Suggested fix. Only treat the word as a type prefix where a type can appear (after new, in
createObject, in declarations), or never when the parser is reading a struct-literal key.
Document it under 7.0 breaking changes until then.