Skip to content

fix(config): catch failing config/*.cfm includes at boot instead of masked 500 - #3070

Merged
bpamiri merged 2 commits into
developfrom
fix/bot-3063-documented-config-services-cfm-snippet-var-di-inje
Jun 12, 2026
Merged

fix(config): catch failing config/*.cfm includes at boot instead of masked 500#3070
bpamiri merged 2 commits into
developfrom
fix/bot-3063-documented-config-services-cfm-snippet-var-di-inje

Conversation

@wheels-bot

@wheels-bot wheels-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Hardens config-file loading so a config/*.cfm template that fails to compile or run during application start no longer takes the whole app down at boot. vendor/wheels/Global.cfc::$includeConfig() now wraps the savecontent { include … } in a try/catch: on failure it logs an error naming the offending template and continues, instead of letting the exception cascade out of onApplicationStart into a masked, application-wide HTTP 500 whose secondary onError failure (Element WHEELS.ENGINEADAPTER is undefined …) hid the real cause.

The canonical trigger is the documented DI snippet var di = injector(); in config/services.cfm, which is a compile error on Adobe CF (top-level var in an included template) while Lucee accepts it — so the documented pattern booted on Lucee and bricked every request on Adobe. This change makes that failure debuggable; the docs snippet itself (changing varlocal./unscoped in the dependency-injection guide and the CLAUDE.md DI block) is the second half of the issue and is handled separately by bot-update-docs.yml, which is why this uses Refs rather than Fixes.

Refs #3063

Related Issue

Refs #3063

Type of Change

  • Bug fix
  • New feature
  • Enhancement to existing feature
  • Documentation update
  • Refactoring

Feature Completeness Checklist

  • DCO sign-off -- commit carries Signed-off-by:
  • Tests -- vendor/wheels/tests/specs/global/includeConfigSpec.cfc (failing → passing)
  • Framework Docs -- handled by bot-update-docs.yml (MDX dependency-injection guide snippet)
  • AI Reference Docs -- handled by bot-update-docs.yml
  • CLAUDE.md -- handled by bot-update-docs.yml (DI Container Quick Reference snippet)
  • Changelog fragment -- changelog.d/3063-includeconfig-compile-guard.fixed.md
  • Test runner passes -- see Test Plan

Test Plan

New spec includeConfigSpec.cfc (3 cases): a config template that fails at runtime is caught (does not propagate), a healthy template still executes its body after the guard, and a later config include still runs after an earlier one fails. A runtime throw (undefined-variable reference) stands in for Adobe's engine-specific compile error so the regression is portable to the Lucee CI runner.

  • Before the fix: does not propagate a failure thrown by a config template and recovers and keeps loading later config after one file fails both failed — the exception propagated out of $includeConfig.
  • After the fix: targeted run of the global spec layer (wheels.tests.specs.global) — 4 passed, 0 failed, 0 errors.
  • Full-suite run shows two pre-existing, unrelated failures (provides sets controller class data format-list pollution; should fallback to template for ajax request with no layout specified); both were present before this change and are outside config loading.

Cross-engine note: the try/catch is uniform across Lucee/Adobe/BoxLang (no engine-specific branches), and the catch body avoids local.X-in-catch persistence and other CLAUDE.md cross-engine traps.

Screenshots / Output

n/a

…asked 500

Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
…mpatibility

Top-level `var di = injector();` in config/services.cfm is a compile error on
Adobe CF (the file is included inside $includeConfig(), a function context).
Change all config/services.cfm code-block examples across guides and CLAUDE.md
to use `local.di = injector(); local.di.map(...)` — valid on all engines.

Refs #3063

Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
@wheels-bot

wheels-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Wheels Bot — Docs updated

Added a doc commit to this PR:

  • web/sites/guides/src/content/docs/v4-0-0/core-concepts/dependency-injection.mdx — changed var di = injector(); to local.di = injector(); in the registration example
  • web/sites/guides/src/content/docs/v4-0-0/digging-deeper/dependency-injection-usage.mdx — same fix in all four config/services.cfm code blocks
  • web/sites/guides/src/content/docs/v4-0-0/digging-deeper/authentication-patterns.mdx — same fix in the authenticator registration block
  • web/sites/guides/src/content/docs/v4-0-0/deployment/observability-and-logging.mdx — same fix in the logger registration block
  • web/sites/guides/src/content/docs/v4-0-0/start-here/tutorial/06-authentication.mdx — same fix in the Step 1 code block and two prose references
  • CLAUDE.md — same fix in the DI Container Quick Reference block

config/services.cfm is included inside $includeConfig(), a function context. A top-level var di = injector(); in that context is a compile error on Adobe CF (it boots fine on Lucee, which masks the issue). Changing all examples to local.di = injector(); local.di.map(...) is valid on every supported engine.

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

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 wraps $includeConfig()'s savecontent/include in a try/catch so a config/*.cfm file that fails to compile or run at boot is logged by name and skipped, instead of cascading out of onApplicationStart into the masked app-wide 500 from #3063. The implementation matches the issue's proposed direction exactly, the new spec (vendor/wheels/tests/specs/global/includeConfigSpec.cfc) follows established prior art (reloadGlobalsSpec.cfc's _tmp fixture + var ctx shared-struct patterns), the changelog fragment and commit message conform, and CI is green. I found no correctness, cross-engine, or security issues — verdict: comment (two non-blocking nits below).

Things I explicitly verified during review:

  • No fail-open security regression from skipping settings.cfm. An empty reloadPassword disables URL-based reload and environment switching rather than allowing them (vendor/wheels/events/onapplicationstart.cfc:371-376), so a swallowed settings.cfm failure fails closed on that surface.
  • Log-and-continue is the issue-sanctioned design, not a unilateral trade-off: #3063's acceptance criteria are "a log line naming the file, not an opaque app-wide 500", and the log text at vendor/wheels/Global.cfc:254 interpolates arguments.template, satisfying that.
  • Catch body avoids the documented cross-engine traps: no local.X-in-catch persistence reads (invariant 11), no loops in finally (invariant 12), no bare tag-in-script statements (invariant 13), engine-uniform constructs only. The inner best-effort try/catch around writeLog mirrors the existing output-leak branch at Global.cfc:268-282.
  • Spec fixture paths are deliberately all-lowercase to survive LCase() + case-sensitive filesystems, and $assert.notThrows has prior art in four other specs.

Docs

  1. Stale docblock on $includeConfigvendor/wheels/Global.cfc:210-232. The docblock still describes only the output-capture behavior, and line 223 narrows the no-throw guarantee to output:

    The function never throws on output; missing-wrapper mistakes are recoverable…

    The function's contract is now broader: it never throws on a template compile/runtime failure either. The rationale lives only in the inline catch comment (lines 241-249), which a caller reading the docblock won't see. Suggest adding one sentence to the docblock, e.g. "The function also never throws on a template compile/runtime failure — the failure is logged with the file name and the file is skipped (issue #3063)."

  2. Behavior change worth a line in the user-facing guide: any exception a config file throws at boot — including an intentional fail-fast guard like throw("missing API key") in config/settings.cfm — is now downgraded to a log entry and the app boots without that file's registrations. The PR delegates doc updates to bot-update-docs.yml for the DI snippet; please make sure that pass (or a follow-up) also mentions that config-file errors are non-fatal at boot as of this change, since the changelog fragment is the only user-facing record of it right now.

Tests

  • The targeted engine wasn't exercised by this PR's CI. The checks that ran are Lucee 7 + SQLite plus Lucee 7 smoke; the bug is an Adobe-CF-specific compile failure, and the spec substitutes a portable runtime throw (a reasonable stand-in, clearly justified in the spec comments). Standard CFML semantics say an include-site compile error is catchable at the include statement, so I expect this to hold — but since CLAUDE.md asks for Adobe verification of Adobe-targeted fixes before relying on CI, a one-off tools/test-matrix.sh adobe2023 mysql run (or replaying the original var di = injector(); repro from #3063 against adobe2023) before merge would close the loop. Non-blocking.

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

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: Re-review at head f0198fe66, which adds one commit on top of the previously reviewed framework fix (b564dce8b, reviewed with state comment — that review's analysis of the $includeConfig() try/catch and includeConfigSpec.cfc still stands and is not repeated here). The new commit switches every config/services.cfm snippet in the guides and CLAUDE.md from var di = injector(); to local.di = injector();. The substitution is correct and the right fix for the documented Adobe CF cliff: config files are included inside $includeConfig() (a function context), where local. assignment is valid on Lucee, Adobe, and BoxLang, while top-level var is an Adobe compile error (#3063). The guides' {test:compile} verification passed (Deploy guides green), commitlint and DCO pass for both commits, and I found no correctness, cross-engine, or security issues. Verdict: comment — the only findings are non-blocking docs nits, including two carried forward unaddressed from the previous review.

What I verified on the delta:

  • Scope correctness of local.divendor/wheels/Global.cfc:237 includes the template inside the $includeConfig function body, so local resolves to the function-local scope on every engine; the snippet change cannot collide with the function's own locals (local.$wheelsConfigOutput, local.preview, …). The tutorial variant (06-authentication.mdx) keeps its <cfscript> wrapper, and the surrounding prose that quoted the old snippet (the leak description and the "service not found" troubleshooting item) was updated consistently — not just the code blocks.
  • All five touched guide pages + CLAUDE.md now show the same canonical shape; no mixed old/new snippets within a page.
  • Commit message (docs: use local.di in config/services.cfm DI snippets for Adobe CF compatibility) conforms to commitlint.config.js (valid type, header ≤ 100 chars, body explains the why) and CI's "Validate Commit Messages" passed.

Docs

  1. Published blog post still teaches the Adobe-breaking snippetweb/content/blog/posts/from-wirebox-to-wheelsdi.md:49-59 has a code block explicitly labeled // config/services.cfm — in-house DI, familiar surface starting with var di = injector(); (same in its source skeleton, docs/releases/blog-skeletons/08-wirebox-to-wheelsdi.md:55). The commit deliberately scoped itself to "guides and CLAUDE.md", but the blog ships to users too, and a reader copying that block into config/services.cfm on Adobe gets exactly the #3063 failure (now downgraded to a logged skip, but their registrations still silently don't run). Suggest updating it to local.di in this PR or a follow-up. Relatedly trivial: the missing-wrapper warning text at vendor/wheels/Global.cfc:276 still uses var di = injector(); as its example of leaked text, while the updated tutorial now tells users they'd see local.di = injector(); leak — harmless, but worth aligning if you touch that string again.

  2. Carried forward (previous review, unaddressed): stale $includeConfig docblockvendor/wheels/Global.cfc:223-225 still says "The function never throws on output; missing-wrapper mistakes are recoverable…", describing only the output-capture contract. Since b564dce8b the contract is broader: a template compile/runtime failure is also caught, logged, and skipped. One added sentence in the docblock would save a future maintainer from having to find the rationale in the inline catch comment.

  3. Carried forward (previous review, unaddressed): no user-facing record that config-file errors are non-fatal at boot — this commit fixes the snippets but adds no guide text noting that, as of this change, an exception thrown by a config/*.cfm file at boot (including an intentional fail-fast throw) results in a logged skip rather than a failed start. The changelog fragment remains the only user-facing record. A sentence on core-concepts/dependency-injection.mdx (or the configuration page) would close this.

  4. PR body checklist is now stale — the Feature Completeness Checklist still shows Framework Docs / AI Reference Docs / CLAUDE.md as unchecked, "handled by bot-update-docs.yml", but f0198fe66 performs the guides + CLAUDE.md updates in this PR. Worth ticking those boxes so the record matches the diff.

@bpamiri
bpamiri marked this pull request as ready for review June 12, 2026 14:39
@bpamiri
bpamiri merged commit 2fb562d into develop Jun 12, 2026
16 checks passed
@bpamiri
bpamiri deleted the fix/bot-3063-documented-config-services-cfm-snippet-var-di-inje branch June 12, 2026 14:40
bpamiri added a commit that referenced this pull request Jun 12, 2026
…nfigIncludeFailed (fail-closed) (#3135)

Reworks the #3070 shape per maintainer decision on #3063: $includeConfig()
no longer logs-and-continues when a config template fails to compile or
run. It still logs to the wheels log, then rethrows a named
Wheels.ConfigIncludeFailed error carrying the failing template path and
the original engine message (original type/detail preserved in detail).
The throw is unconditional — no environment branching, no swallowed path.

A production app with a broken config/settings.cfm previously booted
fail-open on framework defaults and served traffic; now application
start aborts with a clear, located, Rails-style boot error. Composes
with the #3108 onError unmasking so the named error renders on the
development error page.

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.

1 participant