fix(config): onError no longer masks app-start exceptions in dev - #3108
Conversation
$getRequestTimeout(), $statusCode(), and $contentType() gated on $hasEngineAdapter() (which checks both application.wheels and the application.$wheels startup-staging struct) but then read application.wheels.engineAdapter unconditionally. During a failed application start the adapter lives only in application.$wheels, so that read threw 'key [ENGINEADAPTER] doesn't exist' (Lucee) / 'Element WHEELS.ENGINEADAPTER is undefined' (Adobe) from inside onError, masking the original exception (e.g. the Wheels.Cors.InvalidConfiguration guard). Route all three readers through the existing $engineAdapter() resolver, which returns the adapter from whichever scope holds it. No behavior change on the happy path; the failed-startup path now surfaces the real error. Fixes #3076 Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Wheels Bot — Reviewer
TL;DR: This PR fixes #3076 — three $hasEngineAdapter()-gated readers in vendor/wheels/Global.cfc ($getRequestTimeout(), $statusCode(), $contentType()) gated on the two-scope check but then read application.wheels.engineAdapter unconditionally, so a failed app start crashed onError and masked the original exception. The fix routes all three through the existing two-scope $engineAdapter() resolver. I verified the diagnosis, the completeness of the audit, and the cross-engine safety of the new spec; I found no blocking issues. Verdict: comment (clean diff, conservative first-pass; final sign-off after the cross-engine matrix confirms Adobe/BoxLang).
Correctness
No findings — verified, not assumed:
$engineAdapter()(vendor/wheels/Global.cfc:2534) checksapplication.wheelsthenapplication.$wheels, exactly as the PR body claims, and on the happy path returnsapplication.wheels.engineAdapter— so the three changed lines (2270, 2287, 2518) are behavior-neutral when the app started normally and only change the failed-startup path.- Audit completeness:
grep -rn '\$hasEngineAdapter()' vendor/wheels/returns exactly four call sites. Three are the ones fixed here; the fourth (Global.cfc:115) already called$engineAdapter(). There are no remaining gate-then-unconditional-read sites of this class. The other rawapplication.wheels.engineAdapterreads (Dispatch.cfc:150,Dispatch.cfc:925,controller/sse.cfc:69) are not$hasEngineAdapter()-gated and only execute during normal request dispatch after a successful start, so they are correctly out of scope for this fix.
Cross-engine
No findings. The new spec (vendor/wheels/tests/specs/global/engineAdapterScopedReadersSpec.cfc) was checked against the documented invariants:
- Catch-body state uses a struct field (
probe.error = e.message), the documented BoxLang-safe pattern (CLAUDE.md invariant 11; same shape asRateLimiterDatabaseSpec.cfc:261). - The
finallyblocks contain only assignments,StructDelete()calls, and anif/else— no loops, so the Lucee 7 finally-loop miscompile (invariant 12) doesn't apply, and the PR reports the full suite green on Lucee 7.0.1.100. ##3076in the describe string is properly escaped;g = application.wo(semicolon-less) matches the established idiom acrosstests/specs/global/(e.g.internalSpec.cfc:5), which already runs on Adobe 2023/2025 in CI.
Tests
The spec is a genuine reproduction: it stages the adapter only in application.$wheels and deletes application.wheels.engineAdapter, which makes $hasEngineAdapter() return true via the staging branch — exactly the state where the old unconditional read threw. State save/restore in finally correctly handles both the pre-existing-staging and no-staging cases. PR documents failing → passing on Lucee 7 + SQLite with the full core suite green (4424 pass).
One non-blocking nit: the three it() blocks duplicate ~10 lines of save/stage/restore scaffolding each; a spec-local private helper would shrink the file, but the explicit form is also defensible for a regression spec and other suite specs inline similar scaffolding. No change requested.
Docs
Changelog fragment present and correctly named (changelog.d/3076-onerror-engineadapter-scope.fixed.md, type fixed); no direct CHANGELOG.md edit. PR checklist is filled honestly (docs items deferred to bot-update-docs.yml, consistent with the bot pipeline). No convention change, so no CLAUDE.md update needed.
Commits
Single commit fix(config): onError no longer masks app-start exceptions in dev — valid type, subject under 100 chars, body explains the why (gate/read scope mismatch), Fixes #3076 reference, DCO Signed-off-by present. Conformant.
Adobe 2023/2025 and BoxLang confirmation rides on the compat matrix; the fix delegates to the already-cross-engine $engineAdapter() resolver, so risk there is low.
Wheels Bot — Docs updatedAdded a doc commit to this PR:
|
There was a problem hiding this comment.
Wheels Bot — Reviewer
TL;DR: Re-review at the new head, which adds one docs commit (267c57a38, the .ai/wheels/troubleshooting/common-errors.md entry) on top of the previously reviewed fix for #3076. I re-verified the fix's claims independently rather than inheriting the earlier review, and reviewed the docs delta fresh: the diagnosis is accurate, the call-site audit is complete, the new spec follows the documented cross-engine-safe patterns, and both commits are conformant. No blocking findings. Verdict: comment — the diff is clean, but the Adobe 2023 smoke legs and cross-engine matrix are still pending on this head at review time (Lucee 7 + SQLite and commitlint already pass).
Correctness
No findings — each claim re-verified against the head:
$engineAdapter()(vendor/wheels/Global.cfc:2534-2552) resolvesapplication.wheels.engineAdapterfirst, thenapplication.$wheels.engineAdapter, exactly as the PR body states. On the happy path the three changed lines (Global.cfc:2270,:2287,:2518) return the same object as before; only the failed-startup path (adapter staged in$wheelsonly) changes, and it now resolves instead of throwing.- Audit completeness:
$hasEngineAdapter()has exactly four framework call sites (Global.cfc:115,:2269,:2286,:2517). The three gate-then-unconditional-read sites are fixed here; the fourth already called$engineAdapter(). The remaining rawapplication.wheels.engineAdapterreads (Dispatch.cfc:150,Dispatch.cfc:925,controller/sse.cfc:69) are not$hasEngineAdapter()-gated and execute only during normal post-startup dispatch — correctly out of scope. - The gate-then-resolve pairing cannot hit
$engineAdapter()'sWheels.EngineAdapterNotInitializedthrow:$hasEngineAdapter()is true iff one of the two branches the resolver checks holds.
Cross-engine
No findings. The new spec vendor/wheels/tests/specs/global/engineAdapterScopedReadersSpec.cfc was checked against the documented invariants:
- Catch-body state uses a struct field (
probe.error = e.message, e.g. line 36) — the BoxLang-safe pattern (CLAUDE.md invariant 11). - The
finallyblocks contain only assignments,StructDelete()calls, and anif/else— no loops, so the Lucee 7 finally-loop miscompile (invariant 12) does not apply. ##3076in the describe string is properly#-escaped; the semicolon-lessg = application.woopener matches the established idiom in this directory (tests/specs/global/internalSpec.cfc:5), which already runs on Adobe/BoxLang in CI.
The fix itself delegates to the already-cross-engine $engineAdapter() resolver, so engine risk is low; final confirmation rides on the pending matrix.
Tests
The spec is a genuine reproduction of the failed-startup state: it stages the adapter only in application.$wheels and deletes application.wheels.engineAdapter, which keeps $hasEngineAdapter() true via the staging branch — exactly the state where the old unconditional read threw. State restore happens in finally before the expectations run, and handles both the pre-existing-staging and no-staging cases. PR documents failing → passing on Lucee 7 + SQLite with the full core suite green (4424 pass).
Carried-over non-blocking nit: the three it() blocks each inline ~10 lines of identical save/stage/restore scaffolding; a spec-local helper would shrink the file. Explicit scaffolding is also a defensible style for a regression spec — no change requested.
Docs
The new commit's common-errors.md entry (lines 32-44) is accurate against the code: it names the three helpers, correctly describes the two-scope gate vs single-scope read mismatch, sits under the right section ("Startup / Initialization Errors") next to the closely related #2774 entry, and follows that entry's structure (symptom block, cause with PR link, resolution). Changelog fragment changelog.d/3076-onerror-engineadapter-scope.fixed.md is present with a valid type and there is no direct CHANGELOG.md edit. PR checklist is filled honestly (guides/CLAUDE.md deferred to bot-update-docs.yml, consistent with the bot pipeline).
Commits
Both commits conform to commitlint.config.js (Validate Commit Messages already passes on this head):
fix(config): onError no longer masks app-start exceptions in dev— valid type, subject < 100 chars, body explains the why,Fixes #3076, DCO sign-off present.docs: add [ENGINEADAPTER] startup-masking error entry to common-errors— valid type, subject < 100 chars, body links the symptom to #3108/#3076, DCO sign-off present.
Nothing blocking. Once the Adobe 2023 smoke legs and the cross-engine matrix confirm this head, the PR is in good shape to merge.
…nfigIncludeFailed (fail-closed) (#3135) Reworks the #3070 shape per maintainer decision on #3063: $includeConfig() no longer logs-and-continues when a config template fails to compile or run. It still logs to the wheels log, then rethrows a named Wheels.ConfigIncludeFailed error carrying the failing template path and the original engine message (original type/detail preserved in detail). The throw is unconditional — no environment branching, no swallowed path. A production app with a broken config/settings.cfm previously booted fail-open on framework defaults and served traffic; now application start aborts with a clear, located, Rails-style boot error. Composes with the #3108 onError unmasking so the named error renders on the development error page. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
When any exception was thrown during application start (settings/routes load), the development error page masked the original failure:
onErroritself crashed withkey [ENGINEADAPTER] doesn't exist(Lucee) /Element WHEELS.ENGINEADAPTER is undefined(Adobe), so the real error (e.g. theWheels.Cors.InvalidConfigurationguard) never reached the page.Root cause: three request-lifecycle helpers in
vendor/wheels/Global.cfc—$getRequestTimeout()(the reported line 2518), plus$statusCode()and$contentType()surfaced by the audit the issue requested — gate on$hasEngineAdapter()(which checks bothapplication.wheelsand theapplication.$wheelsstartup-staging struct) but then readapplication.wheels.engineAdapterunconditionally. During a failed startup the adapter lives only inapplication.$wheels(on Adobe,application.wheelswas not even a struct), so the read threw and replaced the original exception.The fix routes all three readers through the existing
$engineAdapter()resolver, which returns the adapter from whichever scope holds it. On the happy path$engineAdapter()returns exactlyapplication.wheels.engineAdapter, so there is no behavior change; only the failed-startup path is corrected, and it now surfaces the real error.Related Issue
Fixes #3076
Type of Change
Feature Completeness Checklist
Signed-off-by:(git commit -s)vendor/wheels/tests/specs/global/engineAdapterScopedReadersSpec.cfcreproduces the failed-startup scope mismatch for all three readers (failing → passing)bot-update-docs.ymlbot-update-docs.ymlbot-update-docs.ymlchangelog.d/3076-onerror-engineadapter-scope.fixed.mdTest Plan
New spec
engineAdapterScopedReadersSpec.cfcsimulates the failed-startup state (adapter staged only inapplication.$wheels, absent fromapplication.wheels) and asserts each reader resolves the adapter instead of throwing.Expected [] but received [key [ENGINEADAPTER] doesn't exist]— the exact masking error from the issue./wheels/core/tests).Verified locally on Lucee 7.0.1.100. The fix is engine-agnostic (it delegates to the existing two-scope
$engineAdapter()resolver), so the identical Adobe 2023 failure (Element WHEELS.ENGINEADAPTER is undefined, same root cause) is resolved the same way; the cross-engine CI matrix will confirm Adobe/BoxLang.