Skip to content

fix(config): onError no longer masks app-start exceptions in dev - #3108

Merged
bpamiri merged 2 commits into
developfrom
fix/bot-3076-onerror-masks-every-app-start-exception-in-dev-get
Jun 12, 2026
Merged

fix(config): onError no longer masks app-start exceptions in dev#3108
bpamiri merged 2 commits into
developfrom
fix/bot-3076-onerror-masks-every-app-start-exception-in-dev-get

Conversation

@wheels-bot

@wheels-bot wheels-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

When any exception was thrown during application start (settings/routes load), the development error page masked the original failure: onError itself crashed with key [ENGINEADAPTER] doesn't exist (Lucee) / Element WHEELS.ENGINEADAPTER is undefined (Adobe), so the real error (e.g. the Wheels.Cors.InvalidConfiguration guard) 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 both application.wheels and the application.$wheels startup-staging struct) but then read application.wheels.engineAdapter unconditionally. During a failed startup the adapter lives only in application.$wheels (on Adobe, application.wheels was 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 exactly application.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

  • Bug fix

Feature Completeness Checklist

  • DCO sign-off -- commit carries Signed-off-by: (git commit -s)
  • Tests -- vendor/wheels/tests/specs/global/engineAdapterScopedReadersSpec.cfc reproduces the failed-startup scope mismatch for all three readers (failing → passing)
  • Framework Docs -- handled separately by bot-update-docs.yml
  • AI Reference Docs -- handled separately by bot-update-docs.yml
  • CLAUDE.md -- handled separately by bot-update-docs.yml
  • Changelog fragment -- changelog.d/3076-onerror-engineadapter-scope.fixed.md
  • Test runner passes -- see Test Plan

Test Plan

New spec engineAdapterScopedReadersSpec.cfc simulates the failed-startup state (adapter staged only in application.$wheels, absent from application.wheels) and asserts each reader resolves the adapter instead of throwing.

  • Before the fix (Lucee 7 + SQLite): all three specs fail with Expected [] but received [key [ENGINEADAPTER] doesn't exist] — the exact masking error from the issue.
  • After the fix: the three specs pass, and the full core suite is green — 4424 pass, 0 fail, 0 error (Lucee 7 + SQLite, /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.

$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>

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) checks application.wheels then application.$wheels, exactly as the PR body claims, and on the happy path returns application.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 raw application.wheels.engineAdapter reads (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 as RateLimiterDatabaseSpec.cfc:261).
  • The finally blocks contain only assignments, StructDelete() calls, and an if/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.
  • ##3076 in the describe string is properly escaped; g = application.wo (semicolon-less) matches the established idiom across tests/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.

Documents the Lucee/Adobe symptom fixed by #3108 (issue #3076): when any
app-start exception triggered onError, three request-lifecycle helpers read
application.wheels.engineAdapter unconditionally and masked the real error.

Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
@wheels-bot

wheels-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Wheels Bot — Docs updated

Added a doc commit to this PR:

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) resolves application.wheels.engineAdapter first, then application.$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 $wheels only) 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 raw application.wheels.engineAdapter reads (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()'s Wheels.EngineAdapterNotInitialized throw: $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 finally blocks contain only assignments, StructDelete() calls, and an if/else — no loops, so the Lucee 7 finally-loop miscompile (invariant 12) does not apply.
  • ##3076 in the describe string is properly #-escaped; the semicolon-less g = application.wo opener 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.

@bpamiri
bpamiri marked this pull request as ready for review June 12, 2026 14:07
@bpamiri
bpamiri merged commit 5af7248 into develop Jun 12, 2026
11 checks passed
@bpamiri
bpamiri deleted the fix/bot-3076-onerror-masks-every-app-start-exception-in-dev-get branch June 12, 2026 15:41
bpamiri added a commit that referenced this pull request Jun 12, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant