Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .ai/wheels/cross-engine-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
28 changes: 28 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" | \
Expand Down
1 change: 1 addition & 0 deletions changelog.d/3379-onapplicationend-scope-guard.fixed.md
Original file line number Diff line number Diff line change
@@ -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)
24 changes: 20 additions & 4 deletions cli/lucli/templates/app/public/Application.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
20 changes: 16 additions & 4 deletions examples/starter-app/public/Application.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
20 changes: 16 additions & 4 deletions examples/tweet/public/Application.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
24 changes: 20 additions & 4 deletions public/Application.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
142 changes: 142 additions & 0 deletions vendor/wheels/tests/specs/cli/OnApplicationEndScopeGuardSpec.cfc
Original file line number Diff line number Diff line change
@@ -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, "<!---[\s\S]*?--->", "", "all");
stripped = reReplace(stripped, "/\*[\s\S]*?\*/", "", "all");
stripped = reReplace(stripped, "(?m)//[^\n]*", "", "all");
return stripped;
}

}
Loading