fix(config): route onApplicationEnd through applicationScope.wo to survive Adobe teardown - #3380
Conversation
…rvive 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>
…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>
Wheels Bot — Docs updatedAdded a doc commit to this PR:
No MDX user guide page was updated: no existing v4-0-0 guide page covers |
There was a problem hiding this comment.
Wheels Bot — Reviewer
TL;DR — This PR hardens onApplicationEnd() against Adobe CF 2023 applicationStop() teardown, where the live application scope can resolve application.wo to a stale Java String[] and throw Element wo is undefined in a Java object of type class [Ljava.lang.String;. It routes the onapplicationend.cfm include through the dependable arguments.applicationScope.wo (already used for the $wheelsBrowserLauncher cleanup in the same handler) and guards it with StructKeyExists. The change is correct, cross-engine safe, and mirrors an established prior-art pattern. Verdict: comment — the fix is clean; one minor test-coverage observation below, nothing blocking.
Tests
Solid structural guard. vendor/wheels/tests/specs/cli/OnApplicationEndScopeGuardSpec.cfc faithfully mirrors OnErrorFallbackGuardSpec (#2773) — same brace-matched body extraction, same $stripCfmlComments (CLAUDE.md anti-pattern #14), same private-helper-called-from-it()-closure shape that already ships and passes on every engine. Given the failure only manifests during real Adobe teardown (uncatchable in a spec without killing the runner), a source-shape guard is the right call.
Minor: the guard patches four files but only asserts two.
var targets = [
"cli/lucli/templates/app/public/Application.cfc",
"public/Application.cfc"
];examples/starter-app/public/Application.cfc and examples/tweet/public/Application.cfc receive the identical fix but aren't covered, so a future revert of those two would go uncaught. This is consistent with the mirrored OnErrorFallbackGuardSpec (which also targets only these two) and the PR body scopes the guard to "the CLI template and the repo's own demo app," so it is deliberate — noting only in case extending targets to the example apps is cheap and worthwhile. Not blocking.
Conventions
No issues. The guard checks wo, wheels, and wheels.eventPath before the dereference; && short-circuits, so arguments.applicationScope.wheels.eventPath is only touched once wheels is confirmed present. Happy-path behavior is unchanged — in normal operation application.wo and arguments.applicationScope.wo are the same instance. Changelog fragment changelog.d/3379-onapplicationend-scope-guard.fixed.md uses a valid .fixed type with a complete bullet. Refs #3379 (not Closes) is the right call for a defensive/partial fix.
Cross-engine
No issues. arguments.applicationScope.wo.$include(...) is a method call on a passed-in plain struct + CFC instance, not a function member on the live application scope (invariant #2 does not apply), and it is application code rather than a compiled spec closure (invariant #16b does not apply). The struct ApplicationScope (capital) parameter vs arguments.applicationScope (lower) access is fine — CFML argument names are case-insensitive on all engines.
Commits
Conforms. fix(config): route onApplicationEnd through applicationScope.wo to survive Adobe teardown — valid type/scope, subject well under 100 chars, not ALL-CAPS, body explains the "why," DCO Signed-off-by present.
There was a problem hiding this comment.
Wheels Bot — Reviewer
TL;DR — This PR hardens onApplicationEnd() against Adobe CF 2023 applicationStop() teardown, where the live application scope can resolve application.wo 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. It routes the onapplicationend.cfm include through the passed-in arguments.applicationScope.wo (the same reference the $wheelsBrowserLauncher cleanup in this handler already uses) and guards it with StructKeyExists. The change is correct, precisely scoped, cross-engine safe, and backed by a structural regression guard. Verdict: comment — the fix is clean; one minor, deliberate test-scoping observation below, nothing blocking.
Correctness
No issues. The fix is scoped exactly to the one handler that fires during teardown. Every other application.wo.* dereference (onApplicationStart, onRequestStart, onRequestEnd, onAbort, onError) fires during the normal request lifecycle where the live scope is reliable, so leaving them untouched is correct — git grep "application\.wo" confirms the only occurrences inside onApplicationEnd() are now comments (e.g. public/Application.cfc:161).
The guard short-circuits cleanly with &&:
if (
StructKeyExists(arguments.applicationScope, "wo")
&& StructKeyExists(arguments.applicationScope, "wheels")
&& StructKeyExists(arguments.applicationScope.wheels, "eventPath")
) {so arguments.applicationScope.wheels.eventPath (interpolated into the include template) is only touched once wheels is confirmed present. Happy-path behavior is unchanged — in normal operation application.wo and arguments.applicationScope.wo are the same instance.
Cross-engine
No issues. arguments.applicationScope.wo.$include(...) is a method call on a passed-in plain struct + CFC instance, not a function member on the live application scope (invariant #2 N/A), and it is application code with a non-empty argument list, not a zero-arg call in a compiled spec closure (invariant #16b N/A). The struct ApplicationScope (capital) parameter vs arguments.applicationScope (lower) access is fine — CFML argument names are case-insensitive on all engines. The new spec IIFE loop-capture ((function(relPath){...})(rel)), brace-matched body extraction, and bare-called private $stripCfmlComments are byte-for-byte the shipped OnErrorFallbackGuardSpec.cfc (#2773), which already compiles and passes on every engine in the cli scope — so the spec itself is proven cross-engine safe including Adobe.
Tests
Solid structural guard. vendor/wheels/tests/specs/cli/OnApplicationEndScopeGuardSpec.cfc asserts the source shape across the shipping Application.cfc files: (1) no live application.wo.* dereference in onApplicationEnd(), (2) the include routes through arguments.applicationScope.wo, (3) it is guarded by StructKeyExists(arguments.applicationScope, "wo") before the dereference. It strips comments first ($stripCfmlComments, CLAUDE.md anti-pattern #14), so the comment mention of application.wo on line 161 does not trip check (1). Since the failure only manifests during real Adobe teardown (uncatchable in a spec without killing the runner), a source-shape guard is the right call.
Minor (non-blocking): the fix patches four files but the guard only asserts two —
var targets = [
"cli/lucli/templates/app/public/Application.cfc",
"public/Application.cfc"
];examples/starter-app/public/Application.cfc and examples/tweet/public/Application.cfc receive the identical fix but are not covered, so a future revert of those two would go uncaught. This is consistent with the mirrored OnErrorFallbackGuardSpec (same two targets) and the PR body scopes the guard to "the CLI template and the repo demo app," so it is deliberate — noting only in case extending targets to the example apps is cheap and worthwhile.
Docs
Complete. CLAUDE.md gains cross-engine invariant #19, .ai/wheels/cross-engine-compatibility.md gains a matching section, and changelog.d/3379-onapplicationend-scope-guard.fixed.md is a fragment (not a direct [Unreleased] edit) with a valid .fixed type and a complete bullet. Refs #3379 (not Closes) is the right call for a defensive/partial fix.
Commits
Conform. fix(config): route onApplicationEnd through applicationScope.wo to survive Adobe teardown and docs: document onApplicationEnd application scope teardown hazard on Adobe CF — both valid types (fix, docs), subjects well under 100 chars, not ALL-CAPS, bodies explain the "why."
Summary
On Adobe ColdFusion 2023 the framework's
onApplicationEnd()handler fires synchronously duringapplicationStop()teardown. Inside that teardown the liveapplicationscope is no longer reliable — bareapplication.wocan resolve against a stale/torn-down scope and land on a JavaString[], throwingElement wo is undefined in a Java object of type class [Ljava.lang.String;and erroring the whole site until a CF service restart. This routes theonapplicationend.cfminclude through the passed-inarguments.applicationScope.wo(the only dependable reference at shutdown, already used by the$wheelsBrowserLaunchercleanup in the same handler) and guards it withStructKeyExistsso a partially reclaimed scope degrades to a no-op instead of a hard error. The same edit is applied to the CLIwheels newtemplate, the repo's demo app, and the two bundled example apps; existing apps should apply the same change to theirpublic/Application.cfc.Related Issue
Refs #3379
Type of Change
Feature Completeness Checklist
Signed-off-by:(git commit -s)vendor/wheels/tests/specs/cli/OnApplicationEndScopeGuardSpec.cfc(structural guard, failing → passing)bot-update-docs.ymlbot-update-docs.ymlbot-update-docs.ymlchangelog.d/3379-onapplicationend-scope-guard.fixed.mdwheels.tests.specs.cliscope green (23 bundles, 0 fail / 0 error; the new spec: 2 pass)Test Plan
The failure only manifests on Adobe CF during real
applicationStop()teardown, which cannot be reproduced inside a spec without killing the test runner. The guard is therefore structural:OnApplicationEndScopeGuardSpecscans everyApplication.cfcthat ships the handler (the CLI template and the repo demo app) and assertsonApplicationEnd()(1) never dereferences the liveapplication.wo, (2) routes througharguments.applicationScope.wo, and (3) guards it withStructKeyExists(arguments.applicationScope, "wo")before the dereference. Verified failing before the fix and passing after via:curl -sL "http://localhost:60007/wheels/core/tests?db=sqlite&directory=wheels.tests.specs.cli&format=json"Mirrors the existing
OnErrorFallbackGuardSpec(issue #2773) pattern.