docs(web/guides): correct request-lifecycle stage order, config() lifetime, and failure symptoms - #3065
Conversation
…etime, and failure symptoms Audit-verified corrections to the request-lifecycle concept guide: - Swap stages 1 and 2: dispatch/route match runs before middleware (Dispatch.cfc $paramParser -> $findMatchingRoute precedes the middleware pipeline; only the CORS preflight path runs middleware pre-routing). Middleware only sees matched requests. - config() runs once per application lifetime (cached controller class), not per-request; per-request work is instance init and DI service resolution. - afterAction filters CAN replace the response body via renderText()/ renderView() — the body is not locked in at stage 6. - Replace the fabricated 'Component X has no public method Y' symptom with actual behavior: a missing action falls through to auto-render and throws Wheels.ViewNotFound (404) at stage 7. - Replace 'Blank page but curl -I shows 200' — a missing view is a loud 404 on every engine/env, never a blank 200. - Nits: actual RouteNotFound dev message; [key] route segment syntax (not :key). 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 corrects the request-lifecycle concept guide (web/sites/guides/src/content/docs/v4-0-0/core-concepts/request-lifecycle.mdx) on five behavioral points: stage order (routing before middleware), config() lifetime, afterAction render semantics, and two fabricated failure symptoms. I independently re-verified every claim against framework source on this checkout and they all hold. Verdict: comment — one minor precision nit, nothing blocking.
Verification of the PR's claims (all confirmed)
- Routing before middleware —
$request()calls$paramParser()atvendor/wheels/Dispatch.cfc:382, which calls$findMatchingRoute()at:518; the middleware pipeline runs at:450. The OPTIONS preflight carve-out at:360-379is the only pre-routing middleware path, exactly as the updated prose says. - Middleware sees matched route + params — the request context built at
Dispatch.cfc:419-424carriesparamsandroute(fromrequest.wheels.currentRoute). config()lifetime —config()is invoked inside class init (vendor/wheels/Controller.cfc:59-63) and the class is cached via$doubleCheckedLockincontroller()(vendor/wheels/Global.cfc:1218-1224), so it does not run per-request.- afterAction can replace the body — after-filters run at
vendor/wheels/controller/processing.cfc:123, after the action (:113) and auto-render, with no guard preventing a laterrenderText()/renderView()from overwriting the response. Consistent with the author's live probes. - Removed fabricated symptoms —
grep -rn 'has no public method' vendor/wheels/→ 0 hits; the real behavior isWheels.ViewNotFound("Could not find the view page for the<action>action…",processing.cfc:212). And$throwErrorOrShow404Pagesets$header(statusCode = 404)atGlobal.cfc:2502before throwing/including, so a missing view is never a blank 200. - Nits verified — route-not-found message matches
Dispatch.cfc:270;[key]segment syntax matchesvendor/wheels/mapper/matching.cfc:232,250.
Docs
- Minor precision nit (non-blocking): Stage 3 says
config()"runs once per application lifetime (on the first request after a start or reload)", and the hook table says "once per app start/reload (cached)". Strictly, it runs lazily once per controller class, on the first request that reaches that controller ($createControllerClassvia$doubleCheckedLock,Global.cfc:1218-1224). A reader could infer allconfig()methods fire on the app's first request. Suggested tweak: "once per controller, on the first request that reaches it after a start or reload (then cached)".
Commits
docs(web/guides): correct request-lifecycle stage order, config() lifetime, and failure symptoms— valid type/scope, header under 100 chars. ✓
Docs-only change: no changelog fragment or spec coverage required; no security or cross-engine surface. pnpm verify:docs exit 0 per the PR body (page has no compilable code blocks).
…#3061 The line-109 guarantee ('filters can rely on injected services') is the exact behavior open issue #3061 breaks: onError in public/Application.cfc unconditionally re-creates application.wheelsdi, wiping services.cfm registrations after any uncaught error page. Add a caution Aside citing the issue instead of papering over it. 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 corrects the request-lifecycle concept guide on stage order (routing before middleware), config() lifetime, afterAction render semantics, and two fabricated failure symptoms, and adds a caution Aside tying the beforeAction DI guarantee to open issue #3061. I independently re-verified every behavioral claim against framework source at this head and they all hold; issue #3061 is real and open, and the Aside import is present. Verdict: comment — two minor precision nits, nothing blocking.
Verification (all confirmed at head 3b5789e)
- Routing before middleware —
$paramParser()atvendor/wheels/Dispatch.cfc:382→$findMatchingRoute()at:518; the middleware pipeline runs at:450. The OPTIONS preflight short-circuit atDispatch.cfc:344-379is the only pre-routing middleware path, exactly as the new Stage 2 carve-out paragraph says. - Middleware sees matched route + params — the request context at
Dispatch.cfc:419-424carriesparamsandroutefromrequest.wheels.currentRoute. - afterAction can replace the body — after-filters run via
$runFilters(type="after", ...)atvendor/wheels/controller/processing.cfc:123, after$callActionat:113, with no guard against a laterrenderText()/renderView()overwriting the response. - Fabricated symptoms removed —
grep -rn 'has no public method' vendor/wheels/→ 0 hits; the real missing-action behavior isWheels.ViewNotFound("Could not find the view page for the<action>action…",processing.cfc:212). And$header(statusCode = 404)atGlobal.cfc:2502runs before throw/include, so a missing view is never a blank 200. - Nits verified — RouteNotFound message matches
Dispatch.cfc:270;[key]segment syntax matchesvendor/wheels/mapper/matching.cfc:190-198. - New Aside (#3061) —
public/Application.cfc:345does unconditionally runapplication.wheelsdi = new wheels.Injector("wheels.Bindings")inonError(), and the issue is OPEN. The Aside's claim is accurate in substance (one scoping nit below).
Docs
-
request-lifecycle.mdx:112— the "stray 404" example should be scoped to development. The Aside says "any uncaught error — even a stray 404 error page — re-creates the DI container". A 404 only reachesonErrorwhenshowErrorInformationis true: in production$throwErrorOrShow404Pageincludes the static 404 page andaborts without throwing (vendor/wheels/Global.cfc:2503-2508), so a stray 404 does not wipe the container there. Issue #3061's own summary carefully says "including routine development-mode 404 error pages". Suggested one-word fix: "even a stray 404 error page in development". Non-blocking — the lead clause ("any uncaught error") is accurate everywhere, and genuine uncaught exceptions still trigger the wipe in production. -
request-lifecycle.mdx:70and:99— "once per application lifetime" precision (carried over from my review of030dc29).config()runs lazily, once per controller class, on the first request that reaches that controller ($createControllerClassvia$doubleCheckedLock,vendor/wheels/Global.cfc:1218-1224). "Once per application lifetime (on the first request after a start or reload)" could read as allconfig()methods firing on the app's first request. Suggested: "once per controller, on the first request that reaches it after a start or reload (then cached)". Still non-blocking.
Commits
docs(web/guides): correct request-lifecycle stage order, config() lifetime, and failure symptoms— valid type, header ≤ 100 chars, DCO sign-off matches author. ✓docs(web/guides): caveat the beforeAction DI guarantee with open issue #3061— body explains the why and cites the exact code path (onErrorinpublic/Application.cfc). ✓
Docs-only change: no changelog fragment or spec coverage required; no security or cross-engine surface.
Behavioral-audit corrections for the request-lifecycle concept guide (
web/sites/guides/src/content/docs/v4-0-0/core-concepts/request-lifecycle.mdx). Every change is cite-checked against framework source and live probes on Lucee 7 + Adobe 2023; no editorial rewrites.Corrections
Stages 1 and 2 swapped — dispatch/route match runs BEFORE middleware.
$request()calls$paramParser()→$findMatchingRoute()(vendor/wheels/Dispatch.cfc:382,:518;Wheels.RouteNotFoundthrown at:263-270) beforevariables.$middlewarePipeline.run()(Dispatch.cfc:450). Only the CORS preflight path (Dispatch.cfc:345-381) runs middleware pre-routing — documented as the explicit carve-out. Diagram, stage sections, hook table ("every matched request — unmatched URLs 404 before middleware runs"), "Why the order matters", and the See-also stage references all updated.config()runs once per application lifetime, not per-request.config()runs inside$initControllerClass(vendor/wheels/Controller.cfc:59-63) and the class is cached inapplication.wheels.controllers(Global.cfccontroller()~1209-1230, cleared only on reload). Live probe: oneCONFIG-RANlog line across three requests after a reload. Stage 3 text and the hook table row ("Stage 3, per-request" → "once per app start/reload (cached)") corrected; per-request work described as instance init + DI service resolution.afterAction filters CAN replace the response body. A
renderText()call in an afterAction filter replaced the action's body (HIJACKEDinstead ofpong) on both engines. "The response body is already determined by this point" replaced with accurate wording + a warning against render calls in after-filters.Fabricated symptom removed:
Component X has no public method Y.grep -rn 'has no public method' vendor/wheels/→ 0 hits. Actual behavior: a missing action falls through$callActionto auto-render and throwsWheels.ViewNotFound— "Could not find the view page for the<action>action…" (vendor/wheels/controller/processing.cfc:212) — HTTP 404 at stage 7, on both engines.Fabricated symptom removed: "Blank page but
curl -Ishows 200". A missing view is never a blank 200:$throwErrorOrShow404PagesetsstatusCode=404before throwing (vendor/wheels/Global.cfc:2501-2510). Dev = fullWheels.ViewNotFounderror page, production = generic 404 page; both engines, GET and HEAD. The all-lowercase path advice (verified correct) is kept.Nit: actual route-not-found dev message is "Could not find a route that matched this request." (
Dispatch.cfc:270), typeWheels.RouteNotFound.Nit: Wheels route segment syntax is
[key], not:key.Verification
pnpm verify:docs src/content/docs/v4-0-0/core-concepts/request-lifecycle.mdx→ exit 0 (0 tagged blocks — page is prose + a text diagram, no compilable code blocks).developin this worktree.No open-issue behavior (#3059–#3063) intersects this page — it contains no reload, debug-bar, or
injector()content.🤖 Generated with Claude Code