Skip to content

onError masks every app-start exception in dev: $getRequestTimeout() reads application.wheels.engineAdapter after $hasEngineAdapter() matched application.$wheels #3076

Closed

Description

@bpamiri

Summary

When any exception is thrown during application start (settings/routes load), the development error page does not show the original error. Instead onError itself crashes with key [ENGINEADAPTER] doesn't exist (Lucee 7) / Element WHEELS.ENGINEADAPTER is undefined (Adobe 2023) at vendor/wheels/Global.cfc:2518, hiding the real failure. Reproduced identically on Lucee 7.0.1.100 and Adobe 2023.0.11 (develop @ 840274b).

Concrete victim: the Cors guard Throw(type="Wheels.Cors.InvalidConfiguration") (vendor/wheels/middleware/Cors.cfc:30-37) exists precisely to "fail fast with a clear message", and the CORS guide promises that error type at app start — the user never sees it (Wheels.Cors.InvalidConfiguration appears 0 times in the rendered page, grep-verified). The masking applies to EVERY app-start exception type, not just this one.

Repro

  1. config/settings.cfm: set(middleware = [new wheels.middleware.Cors(allowOrigins="*", allowCredentials=true)]);
  2. Reload/restart in development.
  3. Any request → 500 whose page is the secondary ENGINEADAPTER error from onError (public/Application.cfc:397 → application.wo.$getRequestTimeout()).

Root cause

Global.cfc::$hasEngineAdapter() returns true if the adapter exists in application.wheels OR application.$wheels (the startup staging struct). $getRequestTimeout() then unconditionally reads application.wheels.engineAdapter:

if ($hasEngineAdapter()) {
    return application.wheels.engineAdapter.getRequestTimeout();   // line 2518 — wrong scope when only $wheels matched
}

During a failed startup only the $wheels branch matches (on Adobe, application.wheels wasn't even a struct), so the read throws inside onError and replaces the original exception.

Proposed direction

In $getRequestTimeout(), resolve the adapter from whichever scope actually has it (or wrap the read in the existing engine-native fallback the surrounding code already provides for the "error during startup" case). Audit other $hasEngineAdapter() consumers for the same two-branch-check / one-branch-read mismatch.

Acceptance

  • A deliberate app-start throw (e.g. the Cors misconfiguration above) renders ITS OWN error type/message on the dev error page on Lucee and Adobe.
  • Spec or harness probe covering the failed-startup onError path.

Related but distinct from #3061 (onError DI-container wipe) — same onError code path, different bug.

Reported by the guide-behavioral-audit P1 batch 2 (work item p1-12-cors, claim cors-12; live on both engines, develop @ 840274b).

Activity

  1. wheels-bot commented on Jun 12, 2026

    @wheels-bot
    Contributor

    Wheels Bot — Triage

    Classified as bug.

    The report describes observable wrong behavior with a clear "what happened / what I expected": any app-start exception (e.g. the Wheels.Cors.InvalidConfiguration guard) is masked because onError itself crashes with key [ENGINEADAPTER] doesn't exist (Lucee 7) / Element WHEELS.ENGINEADAPTER is undefined (Adobe 2023), hiding the original failure. Reproduced on both engines at develop @ 840274b2d.

    Suspected layer

    config — app-start / request-lifecycle error handling. The defect is in framework core (vendor/wheels/Global.cfc), reached via the onError path in public/Application.cfc. See .ai/wheels/troubleshooting/ and .ai/wheels/cross-engine-compatibility.md for grounding (there is no dedicated config layer doc).

    Fix sketch

    The root cause is confirmed at vendor/wheels/Global.cfc:2516-2528. $getRequestTimeout() gates on $hasEngineAdapter(), which returns true if the adapter exists in either application.wheels or the application.$wheels startup staging struct — but then unconditionally reads application.wheels.engineAdapter (line 2518), which throws when only the $wheels branch matched (the failed-startup case). The framework already ships the correct resolver immediately below: $engineAdapter() (line 2534) checks both scopes and returns whichever holds the adapter. The mechanical fix is to replace the bare application.wheels.engineAdapter.getRequestTimeout() read with $engineAdapter().getRequestTimeout() (wrapped so a throw still falls through to the existing engine-native fallback at lines 2521-2527). Per the issue's proposed direction, also audit other $hasEngineAdapter()-gated consumers in Global.cfc for the same two-branch-check / one-branch-read mismatch.

    Confidence: medium

    The report is exceptionally clear and the fix is nearly mechanical (an existing helper already does the right thing), but the auto-downgrade rule fires: this is an explicit cross-engine concern (Lucee 7 vs Adobe 2023 differ — on Adobe application.wheels was not even a struct), so confidence is capped at medium.

    <!-- wheels-bot:triage:3076 -->
    <!-- wheels-bot:triage-class:bug -->
    <!-- wheels-bot:triage-confidence:medium -->

  2. wheels-bot commented on Jun 12, 2026

    @wheels-bot
    Contributor

    Wheels Bot — Fix proposed

    Draft PR: #3108

    Spec: vendor/wheels/tests/specs/global/engineAdapterScopedReadersSpec.cfc — failing → passing. It simulates the failed-startup scope mismatch (adapter staged only in application.$wheels) and, pre-fix, reproduces the exact masking error key [ENGINEADAPTER] doesn't exist for all three affected readers.

    Implementation: vendor/wheels/Global.cfc — $getRequestTimeout() (the reported line), plus $statusCode() and $contentType() found by the requested audit, now resolve the adapter through the existing two-scope $engineAdapter() helper instead of reading application.wheels.engineAdapter unconditionally. Happy-path behavior is unchanged; the failed-startup path now surfaces the real exception (e.g. Wheels.Cors.InvalidConfiguration).

    Verified on Lucee 7.0.1.100 + SQLite: the three specs go failing → passing and the full core suite stays green (4424 pass, 0 fail, 0 error). The fix is engine-agnostic, so the matching Adobe failure is resolved the same way; the CI matrix will confirm Adobe/BoxLang.

    A human review is required before merge. The Reviewer will weigh in shortly. Doc updates (MDX guides, .ai/wheels/, CLAUDE.md) are handled separately by bot-update-docs.yml.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions