Lucee 7.1.2.184-RC available for testing

Lucee 7.1.2.184-RC is available for testing. Yes, it’s only a few days after 7.1.1, and as we mentioned then, a fast follow-up was coming. Over the past weeks we fixed a lot of issues and cleared our pull request backlog, so there was no reason to keep those fixes waiting. It’s a large release on top of 7.1.1, with more than 100 tickets. The main areas are AST generation, time zone handling, null handling, extension loading and the Administrator, and six bundled extensions have been updated.

Highlights

  • Security: more Administrator output is encoded, to harden it against XSS.
  • AST generation (LDEV-5715): more than 30 fixes make the AST output match the source faithfully.
  • Time zones: no more Java 25 startup warnings for three-letter IDs (LDEV-6672), dateFormat/timeFormat with an X mask no longer change the JVM time zone (LDEV-6547), Vancouver times are no longer an hour early (LDEV-6570), and the IST ID is fixed (LDEV-5314).
  • Null handling: typed arguments with a null default work when omitted (LDEV-6610), and arrayEach/arrayReduce no longer skip nulls (LDEV-4023).
  • Template cache: inspectTemplates() and pagePoolClear() also clear the application path cache (LDEV-5461).
  • Extensions and Maven: fixes for the “not unique” error on restart after an extension upgrade (LDEV-6145) and “could not load the base component” on a fresh install (LDEV-6666); mavenLoad no longer pulls optional dependencies (LDEV-6639); the Maven download policy message names the correct property (LDEV-6671).
  • Sessions: sessionRotate() rotates JSESSIONID (LDEV-5942), and sessionInvalidate() no longer creates replacement sessions (LDEV-6447).
  • Datasources and queries: query params work with a ? in the SQL (LDEV-4424), CF_SQL_FLOAT works with MSSQL FLOAT (LDEV-6305), no unclosed pool on creation (LDEV-6567), and QueryExecute no longer leaks options as tag attributes (LDEV-6202).
  • Mail: HTML parts are quoted-printable again (LDEV-6545).
  • Administrator: dark mode contrast, extension tiles, restart page layout and the Custom Tags Cancel button (LDEV-5786, LDEV-6616, LDEV-6617, LDEV-5852).
  • Language: method chaining on a returned UDF result (LDEV-4160), calling a lambda in the static scope (LDEV-4792), isClosure() for arrow functions (LDEV-4709), for-in inside finally (LDEV-3393, LDEV-5922), replaceNoCase() with an empty substring (LDEV-5857), and deserialized exceptions keep their stack trace (LDEV-4021).
  • Lucee Light: Server.cfc onBuild() runs on first start (LDEV-6472), and no more spurious /extensions/.index error (LDEV-6665).

Bundled extensions

Extension 7.1.1 7.1.2
MySQL 9.6.0 26.7.0
Microsoft SQL Server 13.2.1 13.2.1
PostgreSQL 42.7.9 42.7.11
Administrator 1.0.0.7 1.0.0.9
Documentation 1.0.0.6 1.0.0.8
S3 2.0.3.1 2.0.3.1
PDF 2.0.1.0 2.0.1.0
Image 3.0.1.1 3.1.0.11
ESAPI 3.0.0.14 3.1.0.1
Scheduler Classic 1.0.0.1 1.0.0.2
Compress 2.1.0.3 2.1.0.3
Mail 1.1.0.17 1.1.0.17
FTP 1.0.0.8 1.0.0.9

The full list of changes is in the GitHub compare.

Please test

This is a release candidate, so please try it with your applications and tell us how it went here, or in Jira. Regressions from 7.1.1 are especially helpful.

all my tests passed with 7.1.2.184-RC and its bundled extensions

Thanks for testing, Kenric, great to hear!

A note for anyone wondering whether to pin the bundled extensions in .CFConfig.json: there’s no need. The bundled extensions are that release’s minimum versions, so Lucee installs or updates them on startup. If you do define extensions in .CFConfig.json, keep in mind the list is treated as the complete set, so Lucee uninstalls anything not in it. The Downloading Extensions recipe shows how to list them.

1 Like

Thanks for the release. I’ve run it in production for the last 36 hours and it seems fine. I also confirmed all the issues I reported are fixed:

LDEV-6472
LDEV-6481
LDEV-6483
LDEV-6579
LDEV-6639
LDEV-6662
LDEV-6665
LDEV-6666
LDEV-6672

1 Like

Thanks Phil, that’s really helpful, especially running it in production for 36 hours. I’ve resolved the tickets that were still waiting in QA (LDEV-6472, LDEV-6579, LDEV-6639, LDEV-6665, LDEV-6666 and LDEV-6672).

Same here - updated my local dev environment and all our tests are passing!

1 Like

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.

  1. 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.)

  2. 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
        }
    }
    
  3. 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.

  1. Fresh server home, install PDF 3.0.0.2 from its .lex (seeds 38 jars into mvn/), no ~/.m2.
  2. Start with LUCEE_MAVEN_DOWNLOAD_POLICY_RUNTIME=error.
  3. <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.

Hi John,

thanks a lot for this detailed write-up, it made verifying much easier. I went through each point on the latest builds (7.1.3.22-SNAPSHOT / 8.0.1.13-SNAPSHOT) and the RCs:

  1. onBuild before extensions are registered – reproduced on 7.1.2.189-RC and 7.1.3.22-SNAPSHOT, onBuild sees no extensions at all. On 8.0.0.294-RC and 8.0.1.13-SNAPSHOT I could not reproduce it, onBuild sees all extensions and encodeForHTML works. Filed as LDEV-6743. Could you share the 8.0 version and config where you saw it?
  2. warm-up exits 0 on failure/timeout – confirmed on 7.1 and 8.0: LDEV-6744
  3. missing resource silently replaced by a download, {lucee-server} not resolved – confirmed on both: LDEV-6745
  4. 13.4.0.jre11 rejected by the Maven provider – confirmed on 7.1 (it then falls back to the old update provider). It’s already fixed in 8.0, so this is a backport to 7.1: LDEV-6746. Where did you see it on 8.0, which build?
  5. resolver keeps every version in the tree – confirmed, for example commons-logging 1.3.0 and 1.3.5 are both pulled in for PDF 3.0.0.2: LDEV-6747. Side note: as far as I can see commons-io:2.17.0 is the only commons-io in the PDF tree (via xmlgraphics-commons 2.11), so that one is legitimately needed.
  6. (8.0) server nullSupport ignored – confirmed on 8.0.0.294-RC, 7.1 is fine: LDEV-6748
  7. (8.0) dotNotationUpperCase ignored – confirmed: LDEV-6749. Until it’s fixed, "preserveCase": true works as a workaround.
  8. { class: [...] } doesn’t compile – confirmed on 7.1 and 8.0, and { cfml: [1], cfc: [2] } even throws an NPE in the compiler: LDEV-6750

Thanks again!
Gian

Wow…that was like watching a first date here between two AIs, clearly love at first sight! :heart_eyes:

3 Likes

Heh heh, I am just the facilitator here. :laughing: I have a followup from Claude, so I am going to keep it going, too.

1 Like

What follows is from Claude after investigating your response:


Thanks for verifying and filing all of these. Two corrections from my side, since you asked:

onBuild before extensions (LDEV-6743): I did not actually observe this on an 8.0 build, so your
non-repro is correct and the “same code on 8.0” line in my write-up was wrong. My warm-up script
only drops Server.cfc in for the second pass, so onBuild never ran during the install pass on
8.0.0.294-RC. I had read that reloadInstance on the 8.0 branch has the same unconditional onStart
call as 7.1, which it does, but on 8.0 nothing on the extension-install path calls reloadInstance
any more (the only callers at 8.0.0.294 are ConfigServerImpl.checkPassword() and the unused
_reloadOLd in ConfigAdmin), so the hook is never reached. Please treat it as 7.1-only.

13.4.0.jre11 (LDEV-6746): same story, not observed on 8.0. I vendor the MSSQL .lex, so the Maven
provider was never asked for that version on 8.0. The 8.0 Version class (reworked 2026-03-20) has
no post-parse null check, while 7.1.2.184 added one at Version.java:253, which is what rejects it.
So the 7.1 backport is the right scope.

commons-io: you are right. openpdf declares commons-io in test scope only, so 2.17.0 via
xmlgraphics-commons 2.11 is the only commons-io in the tree and is legitimately needed. The
commons-io 2.19.0 jar that the pdf-extension-3.0.0.2.lex ships under maven/ is referenced by no
POM in the tree, so it looks like a leftover of the extension build rather than a competing
version. commons-logging 1.3.0 vs 1.3.5 is the right example for LDEV-6747.

I also saw the merge of 6743/44/45/46/47/50 into 8.0 land today. Thanks for the quick turnaround.

We can regard you as the chaperone, or in modern parlance the “wing man”. :slight_smile:

1 Like

Thanks John, that clears it up. LDEV-6743 and LDEV-6746 are 7.1-only then, fixed in 7.1.3.26 and 7.1.3.32. LDEV-6747 now keeps one version per groupId:artifactId (nearest wins), in 7.1.3.29 and 8.0.1.20.

Good catch on the unused commons-io 2.19.0 jar in the PDF .lex. We’ll check the extension build and remove it there.

Since you’re coming from 6.2: we’ll soon make 7.1 the LTS version, and after that 6.2 reaches end of life. If anything else comes up during the upgrade, let us know here. We’re happy to help.