Repository navigation
fix(events): honor explicit set(allowEnvironmentSwitchViaUrl=true) in production-like envs - #3038
Conversation
… 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>
There was a problem hiding this comment.
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 assignmentvariables.$envSwitchUnset = "__wheels_unset__"runs in every invocation context. No mixin/$integrateComponentspath exists for this component that could skip it. - Cross-reload carryover (
onapplicationstart.cfc:156-158) — only ever forces booleanfalse, and the previous app start always left a resolved boolean inapplication.wheels, so the carryover never propagates the sentinel. Unchanged semantics, verified against both branches. - Pre-settings env-revert reader (
onapplicationstart.cfc:236-243) — the newIsBoolean()guard is necessary and correct: without it,!application.$wheels.allowEnvironmentSwitchViaUrlon the string sentinel would throw a cast error. With it, the sentinel keeps the historicaltrue(allow) semantics at that point, and only an explicit/carried-over booleanfalseblocks 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 bothconfig/settings.cfmandconfig/<environment>/settings.cfmincludes and before the atomicapplication.wheels = application.$wheelshandoff, 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.
…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>
What
set(allowEnvironmentSwitchViaUrl=true)inconfig/settings.cfm— the documented override for URL-based environment switching in production-like environments — is now actually honored. Previously the framework default was alsotrueand the hardening guard invendor/wheels/events/onapplicationstart.cfcdetected explicitness by comparing the post-settings value against that default, so explicit-true was indistinguishable from unset and silently discarded inproduction,testing, andmaintenance(compounding #3023 symptom 1: switching into a prod-like environment was a one-way trapdoor).How
__wheels_unset__) instead oftrue. The cross-reload carryover (which only ever forcesfalse) is unchanged.config/settings.cfmincludes, a new public helper$resolveAllowEnvironmentSwitchViaUrl(settingValue, environment)resolves the setting back to a real boolean:falsein production-like environments,trueeverywhere else (both prior behaviors preserved).onapplicationstart.cfc:222) gains anIsBooleanguard: the sentinel there keeps the historicaltrue(allow) semantics; only an explicit/carried-over booleanfalseblocks the switch./wheels/info) only ever see a boolean.Sibling PRs untouched per scope:
public/Application.cfcredirect-strip (#3030) and Dispatch (#3029).Acceptance matrix (from #3031)
false(hardening preserved)truetrue(the fix)falsefalsetruefalsefalseValue is a real boolean in all cases.
Test evidence (Lucee 7 + SQLite, docker harness)
internal.testClientSpeccontainer artifacts.events/EnvironmentSwitchResolutionSpecon unmodified code → HTTP 417, 1 pass / 6 errors (helper absent).environment=testing+set(allowEnvironmentSwitchViaUrl=true), probe viaapp/events/onapplicationstart.cfmhook (runs post-resolution), double reload for fresh templates, on unmodified code:{"environment":"testing","allowEnvironmentSwitchViaUrl":false}— explicit true silently discarded.true; testing + unset →false; testing + explicit false →false; development + unset →true;isBoolean: truein every probe.testClientSpecartifacts remain).All staged configs and the temporary probe hook were restored before commit.
Fixes #3031
🤖 Generated with Claude Code