Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .ai/wheels/troubleshooting/common-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ Wheels failed to initialize. Check the server log for details.

**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.

### "key [ENGINEADAPTER] doesn't exist" / "Element WHEELS.ENGINEADAPTER is undefined" in dev error page
**Error (on-page or in server log):**
```
key [ENGINEADAPTER] doesn't exist (Lucee)
Element WHEELS.ENGINEADAPTER is undefined (Adobe CF)
```

**Cause:** An exception during `onApplicationStart` (e.g. `Wheels.Cors.InvalidConfiguration` from an invalid `config/settings.cfm` value) triggered `onError`, which itself crashed because three request-lifecycle helpers — `$getRequestTimeout()`, `$statusCode()`, and `$contentType()` — read `application.wheels.engineAdapter` directly after gating on `$hasEngineAdapter()`. That gate checks both `application.wheels` and the startup-staging struct `application.$wheels`, but the subsequent read assumed the adapter had been promoted to `application.wheels`. When only the `$wheels` branch matched (the failed-startup state), the read threw and replaced the original exception (fixed in [#3108](https://github.com/wheels-dev/wheels/pull/3108)).

**Resolution:**
The `[ENGINEADAPTER]` crash is a symptom — the real error happened during startup. Check the server log for the original `onApplicationStart` exception; common causes include invalid middleware configuration, a missing CFML mapping, or a syntax error in `config/settings.cfm` or `config/routes.cfm`.

After upgrading past the #3108 fix, `onError` surfaces the original startup exception directly.

## Common Association Errors

### "Missing argument name" in hasMany()
Expand Down
1 change: 1 addition & 0 deletions changelog.d/3076-onerror-engineadapter-scope.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- The development error page no longer masks app-start exceptions: `$getRequestTimeout()`, `$statusCode()`, and `$contentType()` now resolve the engine adapter via `$engineAdapter()` (which checks both `application.wheels` and the `application.$wheels` startup-staging struct) instead of reading `application.wheels.engineAdapter` unconditionally. Previously, when a throw during settings/routes load left the adapter only in `application.$wheels`, `onError` itself crashed with `key [ENGINEADAPTER] doesn't exist` (Lucee) / `Element WHEELS.ENGINEADAPTER is undefined` (Adobe), hiding the original failure such as the `Wheels.Cors.InvalidConfiguration` guard (#3076)
6 changes: 3 additions & 3 deletions vendor/wheels/Global.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -2267,7 +2267,7 @@ return local.$wheels;
*/
public string function $statusCode() {
if ($hasEngineAdapter()) {
return application.wheels.engineAdapter.getStatusCode();
return $engineAdapter().getStatusCode();
}
// Fallback when adapter not yet initialized (e.g. error during startup)
if (StructKeyExists(server, "lucee") || StructKeyExists(server, "boxlang")) {
Expand All @@ -2284,7 +2284,7 @@ return local.$wheels;
*/
public string function $contentType() {
if ($hasEngineAdapter()) {
return application.wheels.engineAdapter.getContentType();
return $engineAdapter().getContentType();
}
// Fallback when adapter not yet initialized
local.rv = "";
Expand Down Expand Up @@ -2515,7 +2515,7 @@ return local.$wheels;
*/
public numeric function $getRequestTimeout() {
if ($hasEngineAdapter()) {
return application.wheels.engineAdapter.getRequestTimeout();
return $engineAdapter().getRequestTimeout();
}
// Fallback when adapter not yet initialized (e.g. error during startup)
if (StructKeyExists(server, "boxlang")) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
component extends="wheels.WheelsTest" {

/*
* Regression coverage for #3076.
*
* During a failed application start, the engine adapter is staged in the
* `application.$wheels` startup struct but never promoted to
* `application.wheels`. Three `$hasEngineAdapter()`-gated readers
* (`$getRequestTimeout()`, `$statusCode()`, `$contentType()`) used to gate
* on the two-scope check but then read `application.wheels.engineAdapter`
* unconditionally. When only the `$wheels` branch matched, that read threw
* `key [ENGINEADAPTER] doesn't exist` (Lucee) /
* `Element WHEELS.ENGINEADAPTER is undefined` (Adobe) from inside `onError`,
* masking the original app-start exception (e.g. the
* `Wheels.Cors.InvalidConfiguration` guard).
*
* Each spec simulates the failed-startup scope mismatch: the adapter lives
* only in `application.$wheels`, and `application.wheels.engineAdapter` is
* absent. The reader must resolve the adapter from whichever scope holds it
* (via `$engineAdapter()`) instead of crashing.
*/
function run() {

g = application.wo

describe("$hasEngineAdapter()-gated readers during a failed startup (##3076)", () => {

it("$getRequestTimeout() resolves the adapter staged in application.$wheels", () => {
var probe = {error = "", value = 0}
var saved = application.wheels.engineAdapter
var hadStaging = StructKeyExists(application, "$wheels")
var savedStaging = hadStaging ? application.$wheels : {}
application.$wheels = {engineAdapter = saved}
StructDelete(application.wheels, "engineAdapter")
try {
probe.value = g.$getRequestTimeout()
} catch (any e) {
probe.error = e.message
} finally {
application.wheels.engineAdapter = saved
if (hadStaging) {
application.$wheels = savedStaging
} else {
StructDelete(application, "$wheels")
}
}
expect(probe.error).toBe("")
expect(probe.value).toBeNumeric()
})

it("$statusCode() resolves the adapter staged in application.$wheels", () => {
var probe = {error = "", value = ""}
var saved = application.wheels.engineAdapter
var hadStaging = StructKeyExists(application, "$wheels")
var savedStaging = hadStaging ? application.$wheels : {}
application.$wheels = {engineAdapter = saved}
StructDelete(application.wheels, "engineAdapter")
try {
probe.value = g.$statusCode()
} catch (any e) {
probe.error = e.message
} finally {
application.wheels.engineAdapter = saved
if (hadStaging) {
application.$wheels = savedStaging
} else {
StructDelete(application, "$wheels")
}
}
expect(probe.error).toBe("")
expect(probe.value).toBeNumeric()
})

it("$contentType() resolves the adapter staged in application.$wheels", () => {
var probe = {error = "", value = "", isSimple = false}
var saved = application.wheels.engineAdapter
var hadStaging = StructKeyExists(application, "$wheels")
var savedStaging = hadStaging ? application.$wheels : {}
application.$wheels = {engineAdapter = saved}
StructDelete(application.wheels, "engineAdapter")
try {
probe.value = g.$contentType()
probe.isSimple = IsSimpleValue(probe.value)
} catch (any e) {
probe.error = e.message
} finally {
application.wheels.engineAdapter = saved
if (hadStaging) {
application.$wheels = savedStaging
} else {
StructDelete(application, "$wheels")
}
}
expect(probe.error).toBe("")
expect(probe.isSimple).toBeTrue()
})
})
}
}
Loading