From af585324d2223ccc5a85b2a532faa9db86790448 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:16:39 +0000 Subject: [PATCH 1/2] fix(config): route onApplicationEnd through applicationScope.wo to survive Adobe teardown On Adobe ColdFusion 2023 the live application scope is unreliable during applicationStop() teardown, so bare application.wo could resolve to a stale Java String[] and throw "Element wo is undefined in a Java object of type class [Ljava.lang.String;", erroring the whole site until a CF service restart. onApplicationEnd() now invokes the Wheels global through the passed-in arguments.applicationScope.wo, guarded with StructKeyExists, so a partially reclaimed scope degrades to a no-op. Applied to the CLI app template, the demo app, and the bundled example apps. Refs #3379 Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- ...3379-onapplicationend-scope-guard.fixed.md | 1 + .../templates/app/public/Application.cfc | 24 ++- examples/starter-app/public/Application.cfc | 20 ++- examples/tweet/public/Application.cfc | 20 ++- public/Application.cfc | 24 ++- .../cli/OnApplicationEndScopeGuardSpec.cfc | 142 ++++++++++++++++++ 6 files changed, 215 insertions(+), 16 deletions(-) create mode 100644 changelog.d/3379-onapplicationend-scope-guard.fixed.md create mode 100644 vendor/wheels/tests/specs/cli/OnApplicationEndScopeGuardSpec.cfc diff --git a/changelog.d/3379-onapplicationend-scope-guard.fixed.md b/changelog.d/3379-onapplicationend-scope-guard.fixed.md new file mode 100644 index 000000000..6a33cfcbe --- /dev/null +++ b/changelog.d/3379-onapplicationend-scope-guard.fixed.md @@ -0,0 +1 @@ +- The application template's `onApplicationEnd()` handler now invokes the Wheels global through the passed-in `arguments.applicationScope.wo` (guarded with `StructKeyExists`) instead of the live `application.wo` scope. On Adobe ColdFusion 2023 the `application` scope is unreliable during `applicationStop()` teardown, so bare `application.wo` could resolve to a stale Java `String[]` and throw `Element wo is undefined in a Java object of type class [Ljava.lang.String;`, erroring the whole site until a CF service restart. The same fix is applied to the repo's demo app and the bundled example apps; existing apps should apply the same edit to their `public/Application.cfc` (#3379) diff --git a/cli/lucli/templates/app/public/Application.cfc b/cli/lucli/templates/app/public/Application.cfc index 93a37230f..45c0dd7a0 100644 --- a/cli/lucli/templates/app/public/Application.cfc +++ b/cli/lucli/templates/app/public/Application.cfc @@ -156,10 +156,26 @@ component output="false" { } } - application.wo.$include( - template = "../../#arguments.applicationScope.wheels.eventPath#/onapplicationend.cfm", - argumentCollection = arguments - ); + // Run the framework's onApplicationEnd event through the Wheels global. + // During applicationStop() teardown on Adobe CF 2023 the LIVE `application` + // scope is unreliable — bare `application.wo` can resolve against a + // stale/torn-down scope and land on a Java String[], throwing "Element wo + // is undefined in a Java object of type class [Ljava.lang.String;" and + // erroring the whole site until a CF service restart (issue #3379). The + // passed-in arguments.applicationScope is the only dependable reference at + // shutdown (it is what the $wheelsBrowserLauncher cleanup above uses), so + // route the call through it and guard so a partially reclaimed scope + // degrades to a no-op instead of a hard error. + if ( + StructKeyExists(arguments.applicationScope, "wo") + && StructKeyExists(arguments.applicationScope, "wheels") + && StructKeyExists(arguments.applicationScope.wheels, "eventPath") + ) { + arguments.applicationScope.wo.$include( + template = "../../#arguments.applicationScope.wheels.eventPath#/onapplicationend.cfm", + argumentCollection = arguments + ); + } } public void function onSessionStart() { diff --git a/examples/starter-app/public/Application.cfc b/examples/starter-app/public/Application.cfc index b945e0deb..99e870afc 100644 --- a/examples/starter-app/public/Application.cfc +++ b/examples/starter-app/public/Application.cfc @@ -128,10 +128,22 @@ component output="false" { } public void function onApplicationEnd( struct ApplicationScope ) { - application.wo.$include( - template = "../../#arguments.applicationScope.wheels.eventPath#/onapplicationend.cfm", - argumentCollection = arguments - ); + // During applicationStop() teardown on Adobe CF 2023 the LIVE `application` + // scope is unreliable — bare `application.wo` can resolve against a + // stale/torn-down scope and land on a Java String[], throwing "Element wo + // is undefined in a Java object of type class [Ljava.lang.String;" (issue + // #3379). The passed-in arguments.applicationScope is the only dependable + // reference at shutdown, so route the call through it and guard it. + if ( + StructKeyExists(arguments.applicationScope, "wo") + && StructKeyExists(arguments.applicationScope, "wheels") + && StructKeyExists(arguments.applicationScope.wheels, "eventPath") + ) { + arguments.applicationScope.wo.$include( + template = "../../#arguments.applicationScope.wheels.eventPath#/onapplicationend.cfm", + argumentCollection = arguments + ); + } } public void function onSessionStart() { diff --git a/examples/tweet/public/Application.cfc b/examples/tweet/public/Application.cfc index b945e0deb..99e870afc 100755 --- a/examples/tweet/public/Application.cfc +++ b/examples/tweet/public/Application.cfc @@ -128,10 +128,22 @@ component output="false" { } public void function onApplicationEnd( struct ApplicationScope ) { - application.wo.$include( - template = "../../#arguments.applicationScope.wheels.eventPath#/onapplicationend.cfm", - argumentCollection = arguments - ); + // During applicationStop() teardown on Adobe CF 2023 the LIVE `application` + // scope is unreliable — bare `application.wo` can resolve against a + // stale/torn-down scope and land on a Java String[], throwing "Element wo + // is undefined in a Java object of type class [Ljava.lang.String;" (issue + // #3379). The passed-in arguments.applicationScope is the only dependable + // reference at shutdown, so route the call through it and guard it. + if ( + StructKeyExists(arguments.applicationScope, "wo") + && StructKeyExists(arguments.applicationScope, "wheels") + && StructKeyExists(arguments.applicationScope.wheels, "eventPath") + ) { + arguments.applicationScope.wo.$include( + template = "../../#arguments.applicationScope.wheels.eventPath#/onapplicationend.cfm", + argumentCollection = arguments + ); + } } public void function onSessionStart() { diff --git a/public/Application.cfc b/public/Application.cfc index 9fd51547d..7887d1922 100644 --- a/public/Application.cfc +++ b/public/Application.cfc @@ -156,10 +156,26 @@ component output="false" { } } - application.wo.$include( - template = "../../#arguments.applicationScope.wheels.eventPath#/onapplicationend.cfm", - argumentCollection = arguments - ); + // Run the framework's onApplicationEnd event through the Wheels global. + // During applicationStop() teardown on Adobe CF 2023 the LIVE `application` + // scope is unreliable — bare `application.wo` can resolve against a + // stale/torn-down scope and land on a Java String[], throwing "Element wo + // is undefined in a Java object of type class [Ljava.lang.String;" and + // erroring the whole site until a CF service restart (issue #3379). The + // passed-in arguments.applicationScope is the only dependable reference at + // shutdown (it is what the $wheelsBrowserLauncher cleanup above uses), so + // route the call through it and guard so a partially reclaimed scope + // degrades to a no-op instead of a hard error. + if ( + StructKeyExists(arguments.applicationScope, "wo") + && StructKeyExists(arguments.applicationScope, "wheels") + && StructKeyExists(arguments.applicationScope.wheels, "eventPath") + ) { + arguments.applicationScope.wo.$include( + template = "../../#arguments.applicationScope.wheels.eventPath#/onapplicationend.cfm", + argumentCollection = arguments + ); + } } public void function onSessionStart() { diff --git a/vendor/wheels/tests/specs/cli/OnApplicationEndScopeGuardSpec.cfc b/vendor/wheels/tests/specs/cli/OnApplicationEndScopeGuardSpec.cfc new file mode 100644 index 000000000..0a53e0161 --- /dev/null +++ b/vendor/wheels/tests/specs/cli/OnApplicationEndScopeGuardSpec.cfc @@ -0,0 +1,142 @@ +/** + * Regression for issue ##3379 — "Element wo is undefined in a Java object of + * type class [Ljava.lang.String;". + * + * On Adobe ColdFusion 2023 the framework's onApplicationEnd() handler fires + * synchronously during applicationStop() teardown (e.g. a ?reload restart or + * an idle-timeout reclaim). Inside that teardown the LIVE `application` scope + * is no longer reliable — bare `application.wo` can resolve against a + * stale/torn-down scope and land on a Java String[] instead of the Wheels + * global, throwing "Element wo is undefined in a Java object of type class + * [Ljava.lang.String;". The whole site then errors until the CF service is + * restarted. + * + * The only dependable reference during shutdown is the passed-in + * arguments.applicationScope (already used for the $wheelsBrowserLauncher + * cleanup in the same handler). The fix routes the onapplicationend.cfm + * include through arguments.applicationScope.wo and guards it with + * StructKeyExists(arguments.applicationScope, "wo") so a partially reclaimed + * scope degrades to a no-op instead of a hard error. + * + * This is a structural guard: the failure only manifests on Adobe CF during + * real teardown, which cannot be reproduced inside a spec without killing the + * runner. So we assert the source shape across every Application.cfc that + * ships the handler — the CLI template (what `wheels new` scaffolds) and the + * repo's own demo app. Mirrors OnErrorFallbackGuardSpec (issue ##2773). + */ +component extends="wheels.WheelsTest" { + + function run() { + + describe("Application.cfc onApplicationEnd scope hardening (issue ##3379)", () => { + + // expandPath("/wheels") resolves to vendor/wheels via the configured + // Lucee mapping; the repo root is two levels above. + var repoRoot = expandPath("/wheels/../.."); + var targets = [ + "cli/lucli/templates/app/public/Application.cfc", + "public/Application.cfc" + ]; + + for (var rel in targets) { + // Capture the loop variable so the closure body binds the + // current value, not the final iteration's value. + (function(relPath) { + it("routes onApplicationEnd through arguments.applicationScope.wo in " & relPath, () => { + var absolute = repoRoot & "/" & relPath; + expect(fileExists(absolute)).toBeTrue("Missing file: " & absolute); + + var raw = fileRead(absolute); + var content = $stripCfmlComments(raw); + + // Extract the onApplicationEnd function body so we don't + // pick up references from other handlers. + var fnMatch = reFindNoCase( + "(?s)public\s+void\s+function\s+onApplicationEnd\s*\([^\)]*\)\s*\{", + content, + 1, + true + ); + expect(fnMatch.len[1] > 0).toBeTrue( + relPath & " should declare a public void onApplicationEnd() function." + ); + + var bodyStart = fnMatch.pos[1] + fnMatch.len[1]; + var depth = 1; + var bodyEnd = bodyStart; + var iEnd = len(content); + for (var i = bodyStart; i <= iEnd; i++) { + var ch = mid(content, i, 1); + if (ch == "{") { + depth++; + } else if (ch == "}") { + depth--; + if (depth == 0) { + bodyEnd = i - 1; + break; + } + } + } + var fnBody = mid(content, bodyStart, bodyEnd - bodyStart + 1); + + // 1. The handler must NOT dereference the live application + // scope — bare `application.wo` is exactly what breaks on + // Adobe CF during teardown. + expect( + reFindNoCase("application\.wo\.", fnBody) == 0 + ).toBeTrue( + relPath & " onApplicationEnd() must not dereference the live " + & "application scope (application.wo.*). During Adobe CF 2023 " + & "teardown that resolves to a stale Java String[] and throws " + & "'Element wo is undefined...' (issue ##3379). Route the call " + & "through arguments.applicationScope.wo instead." + ); + + // 2. The include must be routed through the passed-in + // application scope, the only reliable reference at + // shutdown. + expect( + reFindNoCase("arguments\.applicationScope\.wo\.", fnBody) > 0 + ).toBeTrue( + relPath & " onApplicationEnd() must invoke the Wheels global via " + & "arguments.applicationScope.wo (mirroring the $wheelsBrowserLauncher " + & "cleanup) so it survives teardown on Adobe CF (issue ##3379)." + ); + + // 3. The dereference must be guarded so a partially reclaimed + // scope degrades to a no-op instead of a hard error. + var guardPos = reFindNoCase( + "StructKeyExists\s*\(\s*arguments\.applicationScope\s*,\s*[""']wo[""']\s*\)", + fnBody + ); + var derefPos = reFindNoCase("arguments\.applicationScope\.wo\.", fnBody); + expect(guardPos > 0 && guardPos < derefPos).toBeTrue( + relPath & " onApplicationEnd() must guard " + & "arguments.applicationScope.wo with " + & "StructKeyExists(arguments.applicationScope, ""wo"") before " + & "dereferencing it, so a torn-down scope short-circuits cleanly " + & "(issue ##3379)." + ); + }); + })(rel); + } + + }); + + } + + /** + * Strip CFML tag, block, and line comments before scanning. Mirrors + * the helpers under cli/lucli/services (Analysis.cfc, Doctor.cfc) so a + * commented-out access pattern doesn't pollute the structural check + * (CLAUDE.md anti-pattern ##14). + */ + private string function $stripCfmlComments(required string source) { + var stripped = arguments.source; + stripped = reReplace(stripped, "", "", "all"); + stripped = reReplace(stripped, "/\*[\s\S]*?\*/", "", "all"); + stripped = reReplace(stripped, "(?m)//[^\n]*", "", "all"); + return stripped; + } + +} From e9699f47338fc1bebec151590e4f23972b1726bd Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:20:49 +0000 Subject: [PATCH 2/2] docs: document onApplicationEnd application scope teardown hazard on Adobe CF Add cross-engine invariant #19 to CLAUDE.md and a matching section to .ai/wheels/cross-engine-compatibility.md: during applicationStop() teardown on Adobe CF 2023, bare application.wo can resolve to a stale Java String[] and throw 'Element wo is undefined...'. Route onApplicationEnd() through arguments.applicationScope.wo instead and guard with StructKeyExists (#3379, fixed in #3380). Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- .ai/wheels/cross-engine-compatibility.md | 32 ++++++++++++++++++++++++ CLAUDE.md | 28 +++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/.ai/wheels/cross-engine-compatibility.md b/.ai/wheels/cross-engine-compatibility.md index 7b1f46f50..8c7430bdb 100644 --- a/.ai/wheels/cross-engine-compatibility.md +++ b/.ai/wheels/cross-engine-compatibility.md @@ -36,6 +36,38 @@ plugin.onPluginLoad(context); **Why**: Adobe's application scope is implemented differently from a regular CFML struct. Function members get lost or throw errors during serialization. +### Application Scope Unreliable During onApplicationEnd() Teardown (Adobe CF 2023) + +On Adobe CF 2023, `onApplicationEnd()` fires synchronously during `applicationStop()` teardown (triggered by a `?reload` restart or idle-timeout reclaim). Inside that teardown the live `application` scope is no longer reliable — bare `application.wo` can resolve against a stale/torn-down scope and land on a Java `String[]`, throwing `Element wo is undefined in a Java object of type class [Ljava.lang.String;` and erroring the whole site until a CF service restart. + +The only dependable reference at shutdown is the passed-in `arguments.applicationScope` (already used by the `$wheelsBrowserLauncher` cleanup in the same handler). Route all `onApplicationEnd()` calls through it and guard with `StructKeyExists` so a partially reclaimed scope degrades to a no-op instead of a hard error. Lucee 6/7 and BoxLang are unaffected; this only manifests on Adobe CF during real teardown (issue #3379). + +```cfm +// WRONG — bare application.wo breaks during Adobe CF applicationStop() teardown +public void function onApplicationEnd(struct ApplicationScope) { + application.wo.$include( + template = "../../#arguments.applicationScope.wheels.eventPath#/onapplicationend.cfm", + argumentCollection = arguments + ); +} + +// RIGHT — use the passed-in scope, the only reliable reference at shutdown +public void function onApplicationEnd(struct ApplicationScope) { + if ( + StructKeyExists(arguments.applicationScope, "wo") + && StructKeyExists(arguments.applicationScope, "wheels") + && StructKeyExists(arguments.applicationScope.wheels, "eventPath") + ) { + arguments.applicationScope.wo.$include( + template = "../../#arguments.applicationScope.wheels.eventPath#/onapplicationend.cfm", + argumentCollection = arguments + ); + } +} +``` + +**Existing apps**: apply this same change to `public/Application.cfc` — the CLI template (`wheels new`) and the demo app were fixed in Wheels 4.x (#3380). + ### Closure `this` Captures Declaring Scope CFML closures bind `this` to the component where they are DEFINED, not where they are ASSIGNED. This trips up test code that dynamically adds methods. diff --git a/CLAUDE.md b/CLAUDE.md index 5abe4c375..adcc55f90 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -107,6 +107,34 @@ The framework must run on Lucee 5/6/7, Adobe CF 2018/2021/2023/2025, and BoxLang FileWrite(path, payload); ``` +19. **The `application` scope is unreliable inside `onApplicationEnd()` on Adobe CF 2023.** During `applicationStop()` teardown (triggered by a `?reload` restart or idle-timeout reclaim), bare `application.wo` can resolve against a stale/torn-down scope and land on a Java `String[]`, throwing `Element wo is undefined in a Java object of type class [Ljava.lang.String;` and erroring the whole site until a CF service restart. The only dependable reference at shutdown is the passed-in `arguments.applicationScope`. Always route `onApplicationEnd()` through it and guard with `StructKeyExists` so a partially reclaimed scope degrades to a no-op (#3379). Lucee 6/7 and BoxLang are unaffected; only Adobe CF exhibits this during real teardown. + + ```cfm + // WRONG — bare application.wo breaks on Adobe CF during applicationStop() teardown + public void function onApplicationEnd(struct ApplicationScope) { + application.wo.$include( + template = "../../#arguments.applicationScope.wheels.eventPath#/onapplicationend.cfm", + argumentCollection = arguments + ); + } + + // RIGHT — route through the passed-in scope and guard before dereferencing + public void function onApplicationEnd(struct ApplicationScope) { + if ( + StructKeyExists(arguments.applicationScope, "wo") + && StructKeyExists(arguments.applicationScope, "wheels") + && StructKeyExists(arguments.applicationScope.wheels, "eventPath") + ) { + arguments.applicationScope.wo.$include( + template = "../../#arguments.applicationScope.wheels.eventPath#/onapplicationend.cfm", + argumentCollection = arguments + ); + } + } + ``` + + The CLI template (`wheels new`) and the demo app were fixed in #3380. **Existing apps must apply the same change to their `public/Application.cfc`.** + Verify Adobe CF fixes locally before pushing — don't iterate via CI: ```bash curl -s "http://localhost:62023/wheels/core/tests?db=mysql&format=json" | \