Skip to content

fix(events): defer redirect-after-reload past onApplicationStart so URL env switches persist - #3058

Merged
bpamiri merged 1 commit into
developfrom
peter/issue-3054-redirectafterreload-location
Jun 12, 2026
Merged

fix(events): defer redirect-after-reload past onApplicationStart so URL env switches persist#3058
bpamiri merged 1 commit into
developfrom
peter/issue-3054-redirectafterreload-location

Conversation

@bpamiri

@bpamiri bpamiri commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

?reload=production&password=... (and ?reload=maintenance) — the exact flow in the security-hardening guide — 500'd on the post-restart cold start and the environment switch silently reverted. Two stacked root causes, both fixed here:

  1. Unresolvable bare call. The redirectAfterReload block in vendor/wheels/events/onapplicationstart.cfc called a bare $location(url=..., addToken=false). That file is a plain component { with no extends and no mixins, so $location() (defined in vendor/wheels/Global.cfc:326) is not in scope — every other helper call in the file goes through application.wo. Lucee threw No matching function [$LOCATION] found, the application start aborted, and the engine discarded the half-started app.
  2. Even a resolvable redirect there is wrong. Verified live on Lucee 7: with the call corrected to application.wo.$location(...), cflocation's abort fires while onApplicationStart is still running, the engine discards the half-started application anyway, and the next request cold-starts from config/environment.cfm — the switch still reverted (security log showed "Environment switched to 'production'" followed by development on the next probe).

Fix: the block now stashes the stripped URL on the request scope (request.wheels.redirectAfterReloadUrl) and EventMethods.$runOnRequestStart — which runs in the same request after the new application (including the switched environment) has been persisted — performs the $location() redirect, where it resolves via its wheels.Global inheritance chain. $initializeRequestScope() is idempotent, so the stash survives the onApplicationStart → onRequestStart handoff.

Why exactly production/maintenance broke: events/init/orm.cfm:52-54 auto-enables redirectAfterReload for those two environments, and #3036 (issue #3030) made the restart redirect preserve reload+password for environment switches — which finally made url.reload visible to this block on the cold start. The bare call itself had been latent since the Dec 2024 lifecycle restructure (efb0b1ea8).

Regression spec

vendor/wheels/tests/specs/events/OnAppStartBareHelperGuardSpec.cfc (modeled on BareCfabortGuardSpec):

  • Structural: scans onapplicationstart.cfc for ANY bare $helper( call that is neither defined in the file nor reached via a dotted receiver — guards the whole class of bug, not just $location. Line-anchored, comment-prefix skipping (no global comment-strip regex — that shape hangs Lucee 7). Verified RED against pre-fix source ($location at line 488), GREEN after.
  • Structural: pins the producer/consumer deferral pair (request.wheels.redirectAfterReloadUrl = in onapplicationstart.cfc; StructDelete(request.wheels, "redirectAfterReloadUrl") in EventMethods.cfc).
  • Executable: wheels.events.EventMethods resolves $location (Global inheritance), pinning the deferred call's receiver.

Verification (Lucee 7 docker harness, port 61902, development + set(reloadPassword="smokepw"))

RED (develop @ ddeec99, pre-fix):

  • GET /?reload=production&password=smokepw302 preserving params → followed hop → HTTP 500 ("Something went wrong")
  • wheels-errors.log: No matching function [$LOCATION] found
  • wheels_security.log: Environment switched to 'production' via URL + Reload accepted — the switch engaged, then died
  • Next probe: /wheels/info?format=json200 (reverted to development)

GREEN (this branch):

  • ?reload=production&password=smokepw followed with -L: 2 hops, final 200→404 at http://localhost:61902/reload/password stripped from the final URL
  • Environment sticks: /wheels/info?format=json404 (production); wheels_security.log shows the switch; wheels-errors.log has no new $LOCATION entries (only the timestamped RED-run pair)
  • ?reload=maintenance&password=smokepw: 2 hops → final 503 with the maintenance page ("Sorry, maintenance work is being performed"); subsequent requests stay 503
  • Development + explicit set(redirectAfterReload=true) + ?reload=true&password=smokepw: completes (302 → 200), no errors logged (acceptance criterion 4)
  • Full core suite (/wheels/core/tests?db=sqlite&format=json&reload=true&password=smokepw): 4401 pass / 12 fail / 0 error — the 12 are exactly the known pre-existing internal.testClientSpec container artifacts. New guard spec: 3/3 pass.

Adobe 2023 spot-check (port 61903):

Fixes #3054

🤖 Generated with Claude Code

…RL env switches persist

The redirectAfterReload block in wheels/events/onapplicationstart.cfc called
a bare $location() that does not resolve in that mixin-free component, so the
post-switch cold start threw 'No matching function [$LOCATION] found', 500'd
the request, and the environment switch into production/maintenance silently
reverted (those two environments auto-enable redirectAfterReload via
events/init/orm.cfm). Latent since the Dec 2024 lifecycle restructure
(efb0b1e); became reachable when #3036 made the restart redirect preserve
the reload/password parameters.

Fixing the call resolution alone is not enough: cflocation aborts the request
while onApplicationStart is still running and the engine then discards the
half-started application, reverting the switch anyway (verified live on
Lucee 7). The block now stashes the stripped URL on the request scope and
EventMethods.$runOnRequestStart — same request, after the new application has
been persisted — performs the $location() redirect, where it resolves via the
Global.cfc inheritance chain.

OnAppStartBareHelperGuardSpec pins all three legs: no bare $-helper calls in
the mixin-free onapplicationstart.cfc (line-anchored, comment-skipping scan),
the producer/consumer deferral pair, and $location resolution on
EventMethods.

Fixes #3054

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

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 the 500-and-silent-revert on URL environment switches (?reload=production|maintenance&password=...) by deferring the redirectAfterReload redirect out of onApplicationStart into EventMethods.$runOnRequestStart, where $location() actually resolves and where aborting no longer discards the half-started application. I verified every load-bearing claim in the PR body against the source and could not refute any of them. Verdict: approve. One optional, non-blocking hardening note below.

Correctness

All clean — each mechanism checked against the code rather than the diff hunk:

  • Stash safety: request.wheels is guaranteed to exist at the stash site. onapplicationstart.cfc:20 calls application.wo.$initializeRequestScope() before the redirect block at onapplicationstart.cfc:503, and that helper is guarded with if (!StructKeyExists(request, "wheels")) (vendor/wheels/Global.cfc:2249), so the later call at EventMethods.cfc:327 cannot wipe the stash. The "idempotent" claim in the PR body holds.
  • Receiver resolution: $runOnRequestStart is invoked with componentReference = "wheels.events.EventMethods" (public/Application.cfc and the CLI template), and EventMethods declares extends="wheels.Global" — where $location() is defined (Global.cfc:326). The third spec pins exactly this.
  • Lifecycle order: on the post-restart cold start, onApplicationStart stashes, then the same request's onRequestStart short-circuits the restart path via the environmentSwitchAlreadyApplied check (env now matches url.reload) and reaches $runOnRequestStart, which consumes the stash. The new block correctly guards StructKeyExists(request, "wheels") for ordinary requests where no app start ran.
  • Abort semantics: $location() goes through cflocation with a plain-struct attributeCollection copy (Global.cfc:330-334), so the abort now happens after the application — including the switched environment — has been persisted. onAbort in public/Application.cfc:332 is benign and behaves identically whether the abort fires in app start or request start. Aborting inside the readOnly $simpleLock releases the lock on unwind, same as any abort during request start.
  • No leak: the stash is request-scoped and deleted before the redirect (EventMethods.cfc:167), so it cannot fire twice or bleed across requests.

Conventions

  • Optional, non-blocking: the new line request.wheels.redirectAfterReloadUrl = local.url; (onapplicationstart.cfc:503) reads the pre-existing local.url variable — a reserved-scope name per CLAUDE.md anti-pattern #11, and the same name class behind sibling issue #3053. It is explicitly local.-scoped on every read/write in this block, so it is safe as-is; renaming to local.redirectUrl while touching this block would have been cheap hardening. Fine to leave for a follow-up.
  • $location() is already Adobe-2023/2025-compliant (plain-struct attributeCollection, invariant #10) — no new tag-wrapper exposure introduced.

Tests

OnAppStartBareHelperGuardSpec.cfc is well-constructed and follows documented prior art:

  • Structural scan mirrors security/BareCfabortGuardSpec.cfc (line-anchored, comment-prefix skipping, ListToArray(content, Chr(10), true) — same idiom at BareCfabortGuardSpec.cfc:63). The REFindNoCase(..., true) subexpression form has cross-engine prior art in vendor/wheels/model/validations.cfc:871.
  • The scan guards the whole class of bug (any unresolvable bare $helper( in the mixin-free component), not just $location — good generalization, and the PR body documents RED verification against pre-fix source.
  • All ## escapes inside spec string literals are correct (the classic suite-killer is avoided).
  • The runtime flow genuinely cannot execute inside a spec (cold-start onApplicationStart), so structural pins plus the executable receiver-resolution check is the right coverage shape; the manual RED/GREEN verification on Lucee 7 plus the Adobe 2023 spot-check (with baseline re-verification of the two pre-existing red bundles) fills the gap honestly.

Docs

Changelog fragment present and correctly named (changelog.d/3054-redirectafterreload-deferred-location.fixed.md), with the issue link. No guide changes needed — the fix makes the already-documented security-hardening flow work as written.

Commits

Single commit, valid conventional header (fix(events): ..., 92 chars ≤ 100), DCO sign-off present (Signed-off-by: Peter Amiri). Subject explains the "why" (env switches persist), not just the "what".

@bpamiri
bpamiri merged commit 10687fb into develop Jun 12, 2026
8 checks passed
@bpamiri
bpamiri deleted the peter/issue-3054-redirectafterreload-location branch June 12, 2026 04:22
bpamiri added a commit that referenced this pull request Jun 12, 2026
…rder and env-switch behavior (#3067)

Audit-driven corrections for core-concepts/environments-and-configuration.mdx:

- Fix config load order: app.cfm (pseudo-constructor, every request) ->
  environment.cfm -> framework defaults -> settings.cfm ->
  config/<env>/settings.cfm -> services.cfm -> routes.cfm
- app.cfm executes on every request, not once per app start
- Scaffold hard-codes set(environment="development"); WHEELS_ENV is not
  read by environment.cfm or the wheels CLI; document the two edits
  (env() read + remove scaffold .env line) required to drive it
- Remove set(environment=...) from the settings.cfm example and warn
  about the half-switched-app failure mode
- Replace fictional testing/production "typical settings" rows with the
  real framework defaults; move log rotation / HTTPS to web server /
  SecurityHeaders middleware
- Add runtime environment switching section: ?reload=<env>&password=...,
  allowEnvironmentSwitchViaUrl gate semantics (#3036/#3038/#3058)
- Cite open issues #3059, #3060, #3062 for current caveats

Signed-off-by: Peter Amiri <peter@alurium.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
bpamiri added a commit that referenced this pull request Jun 12, 2026
…ites from behavioral audit (#3068)

Audit of deployment/production-config.mdx against develop (Lucee 7 + Adobe 2023
harness) found seven docs-wrong claims and a batch of stale source citations.
All corrections re-verified against the current develop head (post-#3057/#3058
line shifts).

- Empty reloadPassword does NOT disable ?reload= — only URL env-switching;
  a bare ?reload=true still restarts the app unauthenticated (refs #3062)
- wheels dbmigrate latest -> wheels migrate latest (no dbmigrate verb)
- csrfStore defaults to "session" unconditionally; cookie storage is an
  explicit opt-in (checklist 5 parenthetical was false)
- flash storage selection cite: orm.cfm:57-64, not security.cfm:49-55
- redirectAfterReload cite: orm.cfm:26/:52-54 (also flips for maintenance),
  not security.cfm:43-45
- wheels doctor scope: structure/files/permissions/datasource only — it does
  not audit the production checklist items
- Refresh all stale line refs (settings cascade, env-switch resolver,
  migrate-down gate, settings table, dotenv step 5, secure compare,
  rate limit, maintenance page, boot warning, auto-migrate gate)

verify:docs passes (3 tagged blocks, 0 failed).

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