Skip to content

Commit 1f4d729

Browse files
fix(cli): guard application.wo in onError so init failures don't cascade (#2774)
* fix(cli): guard application.wo in onError so init failures don't cascade When the Wheels Injector fails to load during onApplicationStart (a stale /wheels mapping under Lucee Express 7 is the symptom users hit on the "Your First 15 Minutes" tutorial), application.wo is never assigned. The existing recovery try/catch inside onError swallows a second failure silently and then unconditionally calls application.wo.$getRequestTimeout(), which throws "The key [WO] does not exist." and replaces the real diagnostic with a cryptic cascade. Add a StructKeyExists(application, "wo") guard right after the recovery try/catch in cli/lucli/templates/app/public/Application.cfc (the template behind `wheels new`) and the demo public/Application.cfc. When the global isn't there, render a minimal HTML error page and return — the user sees "Wheels failed to initialize" plus the original exception message instead of the cascade. Fixes #2773 Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * docs(web/guides): note application error fallback and init failure in troubleshooting docs Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * fix(cli): address Reviewer A/B consensus findings (round 1) - Set HTTP 500 status code in the onError fallback in both cli/lucli/templates/app/public/Application.cfc and public/Application.cfc so monitoring tools and CDNs don't cache the Wheels-init failure as a successful response. Uses a plain struct for cfheader's attributeCollection per CLAUDE.md cross-engine invariant #10 (Adobe CF 2023/2025 reject the arguments scope on built-in tags). - Document the no-nested-braces assumption behind catchClosePattern in vendor/wheels/tests/specs/cli/OnErrorFallbackGuardSpec.cfc so a future edit that adds nested braces inside the outer catch knows why the silent fallback to scanFrom=1 is the safety net. - Fix the contradictory recovery steps in the first-15-minutes guide (wheels reload requires a running server) at web/sites/guides/src/content/docs/v4-0-1-snapshot/start-here/first-15-minutes.mdx. - Replace the speculative "pre-4.0.2" wording in .ai/wheels/troubleshooting/common-errors.md with "4.0.1 or earlier" since the fix is still in [Unreleased]. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * chore(web): refresh visual baseline(s) (all) Manually triggered baseline refresh via .github/workflows/refresh-visual-baselines.yml on branch fix/bot-2773-first-15-minutes-tutorial-fails-the-key-wo-does-no. Run when an intentional content/layout change makes the visual-regression check fail. The new PNG(s) under web/tests/visual-baselines/ are now the expected rendering; re-run the failing visual-regression job to flip the check green. --------- Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent d22f97c commit 1f4d729

7 files changed

Lines changed: 241 additions & 0 deletions

File tree

.ai/wheels/troubleshooting/common-errors.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,26 @@ Frequent issues encountered when developing Wheels applications and their soluti
99
- Association syntax has specific Wheels conventions
1010
- Migration parameter binding can be unreliable
1111

12+
## Startup / Initialization Errors
13+
14+
### "Application Error — Wheels failed to initialize"
15+
**Error (on-page):**
16+
```
17+
Application Error
18+
Wheels failed to initialize. Check the server log for details.
19+
<pre>could not find component or class with name [wheels.Injector]</pre>
20+
```
21+
22+
**Cause:** `onApplicationStart` threw before `application.wo` was assigned — typically because the `/wheels` CFML mapping points to a directory that doesn't contain `Injector.cfc`. The `onError` handler now surfaces the original exception text instead of cascading into "The key [WO] does not exist" (fixed in [#2774](https://github.com/wheels-dev/wheels/pull/2774)).
23+
24+
**Resolution:**
25+
1. Read the `<pre>` block — it contains the original error (missing class, path mismatch, etc.).
26+
2. Check the server log for the full stack trace from `onApplicationStart`.
27+
3. Verify the `/wheels` mapping resolves: on a fresh install, `vendor/wheels/Injector.cfc` must exist. If it doesn't, re-run `wheels new` or copy the framework files manually.
28+
4. Run `wheels reload` (or stop/start the server) to pick up the corrected mapping.
29+
30+
**Note:** Before the #2774 fix, this failure cascaded into a second `[WO] does not exist` exception that hid the real cause. If you see the old cascade on a version that predates this fix (i.e. 4.0.1 or earlier), the underlying cause is always a failed `onApplicationStart` — see above.
31+
1232
## Common Association Errors
1333

1434
### "Missing argument name" in hasMany()

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ All historical references to "CFWheels" in this changelog have been preserved fo
1818

1919
----
2020

21+
# [Unreleased]
22+
23+
### Fixed
24+
25+
- `onError` in the generated app template and demo `public/Application.cfc` now guards `application.wo` with `StructKeyExists(application, "wo")` after the recovery try/catch. When `new wheels.Injector(...)` fails during `onApplicationStart` (e.g. a stale `/wheels` mapping under Lucee Express 7), the original error is preserved via a minimal HTML fallback instead of cascading into the cryptic "The key [WO] does not exist" exception that hit "Your First 15 Minutes" tutorial users on fresh installs
26+
27+
----
28+
2129
# [4.0.1](https://github.com/wheels-dev/wheels/releases/tag/v4.0.1) => 2026-05-20
2230

2331
> **Wheels 4.0.1** — first patch on the 4.0 line. Hardens Adobe ColdFusion 2023/2025 compatibility (Adobe-specific `cfheader` attributeCollection rejection, `env()` reserved-word parameter, Vite asset-walk array-by-value), fixes the Windows Scoop install regressions (`wheels.cmd` cmd.exe pre-parser, `.zip.sha512` sidecar layout), and adds `viewStyle` framework presets to `paginationNav()` plus plural `mappings` aliases to `package.json`. ~100 PRs since the 4.0.0 GA (2026-05-12).

cli/lucli/templates/app/public/Application.cfc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,34 @@ component output="false" {
274274
// Must never break error handling
275275
}
276276

277+
// If the Wheels global never came up (e.g. the /wheels mapping is
278+
// stale or Injector.cfc can't be resolved), the original error is
279+
// already lost — fall back to a minimal HTML response rather than
280+
// cascading into "The key [WO] does not exist." (issue ##2773).
281+
if (!StructKeyExists(application, "wo")) {
282+
setting requestTimeout=30;
283+
// Surface a real 5xx so monitoring tools and CDNs don't cache this
284+
// failure as a successful response. Use a plain struct for
285+
// attributeCollection — Adobe CF 2023/2025 reject the `arguments`
286+
// scope on built-in tags (CLAUDE.md cross-engine invariant ##10).
287+
try {
288+
local.statusArgs = {statusCode: 500, statusText: "Internal Server Error"};
289+
cfheader(attributeCollection=local.statusArgs);
290+
} catch (any headerErr) {
291+
// Header may already have been written; the body still renders.
292+
}
293+
WriteOutput("<h1>Application Error</h1>");
294+
WriteOutput("<p>Wheels failed to initialize. Check the server log for details.</p>");
295+
try {
296+
if (isStruct(arguments.Exception) && StructKeyExists(arguments.Exception, "message")) {
297+
WriteOutput("<pre>" & encodeForHTML(arguments.Exception.message) & "</pre>");
298+
}
299+
} catch (any fallbackErr) {
300+
// Last-ditch render must never throw.
301+
}
302+
return;
303+
}
304+
277305
local.requestTimeout = application.wo.$getRequestTimeout() + 30;
278306
if (StructKeyExists(application, "wheels") && StructKeyExists(application.wheels, "onErrorRequestTimeout")) {
279307
local.requestTimeout = application.wheels.onErrorRequestTimeout;

public/Application.cfc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,34 @@ component output="false" {
288288
// Must never break error handling
289289
}
290290

291+
// If the Wheels global never came up (e.g. the /wheels mapping is
292+
// stale or Injector.cfc can't be resolved), the original error is
293+
// already lost — fall back to a minimal HTML response rather than
294+
// cascading into "The key [WO] does not exist." (issue ##2773).
295+
if (!StructKeyExists(application, "wo")) {
296+
setting requestTimeout=30;
297+
// Surface a real 5xx so monitoring tools and CDNs don't cache this
298+
// failure as a successful response. Use a plain struct for
299+
// attributeCollection — Adobe CF 2023/2025 reject the `arguments`
300+
// scope on built-in tags (CLAUDE.md cross-engine invariant ##10).
301+
try {
302+
local.statusArgs = {statusCode: 500, statusText: "Internal Server Error"};
303+
cfheader(attributeCollection=local.statusArgs);
304+
} catch (any headerErr) {
305+
// Header may already have been written; the body still renders.
306+
}
307+
WriteOutput("<h1>Application Error</h1>");
308+
WriteOutput("<p>Wheels failed to initialize. Check the server log for details.</p>");
309+
try {
310+
if (isStruct(arguments.Exception) && StructKeyExists(arguments.Exception, "message")) {
311+
WriteOutput("<pre>" & encodeForHTML(arguments.Exception.message) & "</pre>");
312+
}
313+
} catch (any fallbackErr) {
314+
// Last-ditch render must never throw.
315+
}
316+
return;
317+
}
318+
291319
// In case the error was caused by a timeout we have to add extra time for error handling.
292320
// We have to check if onErrorRequestTimeout exists since errors can be triggered before the application.wheels struct has been created.
293321
local.requestTimeout = application.wo.$getRequestTimeout() + 30;
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* Regression for issue ##2773 — "First 15 Minutes tutorial fails. The key
3+
* [WO] does not exist."
4+
*
5+
* When the Wheels Injector fails to load during onApplicationStart (e.g. a
6+
* stale ##wheels mapping under Lucee 7), application.wo is never assigned.
7+
* onError then fires for the original Injector failure, swallows a second
8+
* failure inside its try/catch, and unconditionally calls
9+
* application.wo.$getRequestTimeout() — which throws "The key [WO] does not
10+
* exist." and replaces the real diagnostic with a cryptic cascade.
11+
*
12+
* The defensive fix is in each Application.cfc whose onError uses the
13+
* try/catch + DI-init pattern: after the catch, guard application.wo with
14+
* StructKeyExists(application, "wo") and short-circuit to a minimal error
15+
* response if the global never came up. Without the guard, init failure
16+
* cascades and hides the actual root cause from the user.
17+
*/
18+
component extends="wheels.WheelsTest" {
19+
20+
function run() {
21+
22+
describe("Application.cfc onError fallback hardening (issue ##2773)", () => {
23+
24+
// expandPath("/wheels") resolves to vendor/wheels via the configured
25+
// Lucee mapping; the repo root is two levels above.
26+
var repoRoot = expandPath("/wheels/../..");
27+
var targets = [
28+
"cli/lucli/templates/app/public/Application.cfc",
29+
"public/Application.cfc"
30+
];
31+
32+
for (var rel in targets) {
33+
// Capture the loop variable so the closure body binds the
34+
// current value, not the final iteration's value.
35+
(function(relPath) {
36+
it("guards application.wo before dereferencing it in onError() in " & relPath, () => {
37+
var absolute = repoRoot & "/" & relPath;
38+
expect(fileExists(absolute)).toBeTrue("Missing file: " & absolute);
39+
40+
var raw = fileRead(absolute);
41+
var content = $stripCfmlComments(raw);
42+
43+
// Extract the onError function body so we don't pick up
44+
// guards from other handlers (e.g. onAbort) that already
45+
// check application.wo.
46+
var onErrorMatch = reFindNoCase(
47+
"(?s)public\s+void\s+function\s+onError\s*\([^\)]*\)\s*\{",
48+
content,
49+
1,
50+
true
51+
);
52+
expect(onErrorMatch.len[1] > 0).toBeTrue(
53+
relPath & " should declare a public void onError() function."
54+
);
55+
56+
var bodyStart = onErrorMatch.pos[1] + onErrorMatch.len[1];
57+
var depth = 1;
58+
var bodyEnd = bodyStart;
59+
var iEnd = len(content);
60+
for (var i = bodyStart; i <= iEnd; i++) {
61+
var ch = mid(content, i, 1);
62+
if (ch == "{") {
63+
depth++;
64+
} else if (ch == "}") {
65+
depth--;
66+
if (depth == 0) {
67+
bodyEnd = i - 1;
68+
break;
69+
}
70+
}
71+
}
72+
var onErrorBody = mid(content, bodyStart, bodyEnd - bodyStart + 1);
73+
74+
// 1. The cascade-guard must exist.
75+
expect(
76+
reFindNoCase(
77+
"StructKeyExists\s*\(\s*application\s*,\s*[""']wo[""']\s*\)",
78+
onErrorBody
79+
) > 0
80+
).toBeTrue(
81+
relPath & " onError() must guard application.wo with "
82+
& "StructKeyExists(application, ""wo"") before dereferencing "
83+
& "it — without the guard a failed Injector init cascades "
84+
& "into ""The key [WO] does not exist."" (issue ##2773)."
85+
);
86+
87+
// 2. The guard must short-circuit before the first
88+
// application.wo.* dereference. Find the position of
89+
// the first such call after the catch block closes,
90+
// and assert the guard appears before it.
91+
//
92+
// Assumption: the outer catch body has no nested
93+
// braces. `[^\}]*` only matches catch bodies whose
94+
// contents (after comment stripping) contain no `{`
95+
// or `}`. If a future edit introduces a conditional
96+
// or nested try inside the outer catch, this regex
97+
// will fail to match and `scanFrom` falls back to 1
98+
// (top of onErrorBody) — the spec still passes as
99+
// long as the guard exists, but the "scan after the
100+
// catch" precision is lost. Widen the pattern (e.g.
101+
// a brace-counter like the one above) if that
102+
// becomes necessary.
103+
var catchClosePattern = "catch\s*\(\s*any\s+\w+\s*\)\s*\{[^\}]*\}";
104+
var catchMatch = reFindNoCase(catchClosePattern, onErrorBody, 1, true);
105+
106+
// If the body has a try/catch at the top of onError, the
107+
// guard must land between the close of that catch and
108+
// the first application.wo.* reference. If it doesn't
109+
// (simpler form), the guard still needs to come before
110+
// the first application.wo.* reference.
111+
var scanFrom = (catchMatch.pos[1] > 0)
112+
? (catchMatch.pos[1] + catchMatch.len[1])
113+
: 1;
114+
var tail = mid(onErrorBody, scanFrom, len(onErrorBody) - scanFrom + 1);
115+
116+
var derefPos = reFindNoCase("application\.wo\.", tail);
117+
var guardPos = reFindNoCase(
118+
"StructKeyExists\s*\(\s*application\s*,\s*[""']wo[""']\s*\)",
119+
tail
120+
);
121+
122+
if (derefPos > 0) {
123+
expect(guardPos > 0 && guardPos < derefPos).toBeTrue(
124+
relPath & " onError() dereferences application.wo "
125+
& "after the recovery try/catch without first "
126+
& "guarding it. The guard must appear before any "
127+
& "application.wo.* call so a failed Injector "
128+
& "init can short-circuit cleanly (issue ##2773)."
129+
);
130+
}
131+
});
132+
})(rel);
133+
}
134+
135+
});
136+
137+
}
138+
139+
/**
140+
* Strip CFML tag, block, and line comments before scanning. Mirrors
141+
* the helpers under cli/lucli/services (Analysis.cfc, Doctor.cfc) so a
142+
* commented-out access pattern doesn't pollute the structural check
143+
* (CLAUDE.md anti-pattern ##14).
144+
*/
145+
private string function $stripCfmlComments(required string source) {
146+
var stripped = arguments.source;
147+
stripped = reReplace(stripped, "<!---[\s\S]*?--->", "", "all");
148+
stripped = reReplace(stripped, "/\*[\s\S]*?\*/", "", "all");
149+
stripped = reReplace(stripped, "(?m)//[^\n]*", "", "all");
150+
return stripped;
151+
}
152+
153+
}

web/sites/guides/src/content/docs/v4-0-1-snapshot/start-here/first-15-minutes.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ You'll see log lines ending with something like `Server started successfully`. O
4747

4848
The dev server stays running in the background. Leave the terminal open.
4949

50+
<Aside type="caution" title="Seeing an Application Error page instead?">
51+
If the browser shows `Application Error — Wheels failed to initialize` rather than the welcome page, the error message on screen includes the underlying cause. Check the server log for the full stack trace. The most common cause on a fresh install is a stale `/wheels` CFML mapping. While the server is running, try `wheels reload` to pick up the corrected mapping. If that does not clear it, stop and restart the server. The server log will point to the specific file that failed to load.
52+
</Aside>
53+
5054
## 3. Add a page (5 min)
5155

5256
<Steps>
8.2 KB
Loading

0 commit comments

Comments
 (0)