Skip to content

fix(events): honor explicit set(allowEnvironmentSwitchViaUrl=true) in production-like envs - #3038

Merged
bpamiri merged 1 commit into
developfrom
peter/issue-3031-envswitch-explicit-override
Jun 11, 2026
Merged

bpamiri merged 1 commit into
developfrom
peter/issue-3031-envswitch-explicit-override

Conversation

@bpamiri

@bpamiri bpamiri commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

What

set(allowEnvironmentSwitchViaUrl=true) in config/settings.cfm — the documented override for URL-based environment switching in production-like environments — is now actually honored. Previously the framework default was also true and the hardening guard in vendor/wheels/events/onapplicationstart.cfc detected explicitness by comparing the post-settings value against that default, so explicit-true was indistinguishable from unset and silently discarded in production, testing, and maintenance (compounding #3023 symptom 1: switching into a prod-like environment was a one-way trapdoor).

How

  • The setting boots as a non-boolean sentinel (__wheels_unset__) instead of true. The cross-reload carryover (which only ever forces false) is unchanged.
  • After the config/settings.cfm includes, a new public helper $resolveAllowEnvironmentSwitchViaUrl(settingValue, environment) resolves the setting back to a real boolean:
    • explicit boolean from settings → honored as-is in every environment (the fix);
    • still the sentinel → framework default: false in production-like environments, true everywhere else (both prior behaviors preserved).
  • The pre-settings env-revert reader (onapplicationstart.cfc:222) gains an IsBoolean guard: the sentinel there keeps the historical true (allow) semantics; only an explicit/carried-over boolean false blocks the switch.
  • The sentinel never escapes a single app start — downstream readers (cross-reload carryover, env revert, /wheels/info) only ever see a boolean.

Sibling PRs untouched per scope: public/Application.cfc redirect-strip (#3030) and Dispatch (#3029).

Acceptance matrix (from #3031)

Environment Setting Resolved
production/testing/maintenance unset false (hardening preserved)
production/testing/maintenance explicit true true (the fix)
production/testing/maintenance explicit false false
development unset true
development explicit false false

Value is a real boolean in all cases.

Test evidence (Lucee 7 + SQLite, docker harness)

  • Baseline (unmodified develop): 4374 pass / 12 fail / 0 error — the 12 are the known pre-existing internal.testClientSpec container artifacts.
  • RED (spec): new events/EnvironmentSwitchResolutionSpec on unmodified code → HTTP 417, 1 pass / 6 errors (helper absent).
  • RED (live): staged environment=testing + set(allowEnvironmentSwitchViaUrl=true), probe via app/events/onapplicationstart.cfm hook (runs post-resolution), double reload for fresh templates, on unmodified code: {"environment":"testing","allowEnvironmentSwitchViaUrl":false} — explicit true silently discarded.
  • GREEN (live, post-fix): testing + explicit true → true; testing + unset → false; testing + explicit false → false; development + unset → true; isBoolean: true in every probe.
  • GREEN (spec): 7/7 pass (HTTP 200).
  • Full suite post-fix: 4381 pass / 12 fail / 0 error (baseline + 7 new specs; only the tolerated testClientSpec artifacts remain).

All staged configs and the temporary probe hook were restored before commit.

Fixes #3031

🤖 Generated with Claude Code

… production-like envs

The allowEnvironmentSwitchViaUrl setting defaulted to true and the
production-hardening guard compared the post-settings value against that
default, so an explicit set(allowEnvironmentSwitchViaUrl=true) in
config/settings.cfm was indistinguishable from 'never set' and silently
discarded in production, testing, and maintenance — the documented
override could not be expressed.

The setting now boots as a non-boolean sentinel; after the settings.cfm
includes, $resolveAllowEnvironmentSwitchViaUrl() resolves it back to a
real boolean: an explicit boolean is honored in every environment, the
sentinel falls back to the framework default (false in production-like
environments, true everywhere else). The pre-settings env-revert reader
gains an IsBoolean guard so the sentinel (historically 'true' at that
point) keeps its old allow semantics, and the cross-reload carryover
still only ever sees the resolved boolean.

Fixes #3031

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 #3031 — an explicit set(allowEnvironmentSwitchViaUrl=true) in config/settings.cfm was indistinguishable from the framework default (also true), so the production-hardening guard in onapplicationstart.cfc silently discarded it in production/testing/maintenance. The sentinel-boot + post-settings-resolution design is sound, all pre-resolution readers are correctly guarded, the new spec covers the full acceptance matrix from #3031, and the commit/changelog hygiene is clean. Verdict: comment — no blocking findings, one minor observation below.

Correctness

I traced every reader of allowEnvironmentSwitchViaUrl at head SHA 37b89ad66 and the design holds:

  • Boot sentinel (vendor/wheels/events/onapplicationstart.cfc:155) — the component is instantiated via DI (public/Application.cfc:115: application.wheelsdi.getInstance(name = "wheels.events.onapplicationstart", ...)), so the pseudo-constructor assignment variables.$envSwitchUnset = "__wheels_unset__" runs in every invocation context. No mixin/$integrateComponents path exists for this component that could skip it.
  • Cross-reload carryover (onapplicationstart.cfc:156-158) — only ever forces boolean false, and the previous app start always left a resolved boolean in application.wheels, so the carryover never propagates the sentinel. Unchanged semantics, verified against both branches.
  • Pre-settings env-revert reader (onapplicationstart.cfc:236-243) — the new IsBoolean() guard is necessary and correct: without it, !application.$wheels.allowEnvironmentSwitchViaUrl on the string sentinel would throw a cast error. With it, the sentinel keeps the historical true (allow) semantics at that point, and only an explicit/carried-over boolean false blocks the switch. I walked the trapdoor scenario from #3023 and the explicit-true escape path; both behave as the PR's acceptance matrix claims.
  • Resolution point (onapplicationstart.cfc:366-369) — runs after both config/settings.cfm and config/<environment>/settings.cfm includes and before the atomic application.wheels = application.$wheels handoff, so /wheels/info (vendor/wheels/public/views/info.cfm:27) and all request-time readers only ever see a boolean.
  • The helper (onapplicationstart.cfc:503-512) also fixes a latent edge: a non-boolean explicit value (e.g. set(allowEnvironmentSwitchViaUrl="banana")) previously survived the old ==-comparison guard as a raw string and would have thrown downstream at !application.$wheels.allowEnvironmentSwitchViaUrl; it now resolves to the framework default.

Minor (non-blocking): there is one window where the sentinel is observable — user code calling get("allowEnvironmentSwitchViaUrl") inside config/settings.cfm itself. $get (vendor/wheels/Global.cfc:632) returns application[$appKey()][name] raw, and $appKey() resolves to $wheels during init, so an unset setting reads back as "__wheels_unset__" mid-include where it previously read true. Anyone branching on it there (if (get("allowEnvironmentSwitchViaUrl"))) would now get a boolean-cast error at startup instead of a stale pre-resolution true. The old value was already unreliable at that point (the retired hardening guard also mutated it post-settings), the failure is loud rather than silent, and reading this setting inside settings.cfm is pathological — so I'm noting it for the record, not requesting a change. If you want belt-and-braces, the helper's doc comment could mention it, but I would also accept this as-is.

Tests

vendor/wheels/tests/specs/events/EnvironmentSwitchResolutionSpec.cfc covers the full acceptance matrix from #3031: unset/explicit-true/explicit-false across all three production-like environments, the development defaults, the always-boolean invariant, plus a live assertion that the running app's resolved setting is a real boolean. It extends wheels.WheelsTest (not legacy RocketUnit), lives in the existing events/ spec directory, and matches the style of sibling specs (SecurityDefaultsSpec.cfc). The helper is public with a $ prefix, which is both the cross-engine mixin convention (CLAUDE.md invariant #7) and what makes it directly testable.

The carryover→revert integration path (reload-driven, requires real app restarts) is covered by the documented live probes in the PR body rather than the spec suite — reasonable given the harness can't restart the application mid-suite, and the in-suite live assertion plus the unit matrix bracket it adequately.

Docs

Changelog fragment present and well-formed: changelog.d/3031-envswitch-explicit-override.fixed.md (correct fixed type, complete bullet, issue reference; no direct CHANGELOG.md edit). No guides update needed — the fix makes already-documented behavior (docs/superpowers/plans/2026-04-16-wheels-4.0-upgrade-guide.md:199 tells users to set(allowEnvironmentSwitchViaUrl=true) explicitly) actually work.

Commits

Single commit, header fix(events): honor explicit set(allowEnvironmentSwitchViaUrl=true) in production-like envs — valid type, 90 chars, explains the why in the body, DCO Signed-off-by matches the author. The PR title matches the commit header, so the squash-merge gate passes as-is.

@bpamiri
bpamiri merged commit 71a2e9f into develop Jun 11, 2026
6 checks passed
@bpamiri
bpamiri deleted the peter/issue-3031-envswitch-explicit-override branch June 11, 2026 20:29
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

events: explicit set(allowEnvironmentSwitchViaUrl=true) is indistinguishable from the default — documented override impossible

1 participant