Skip to content

docs(web/guides): correct request-lifecycle stage order, config() lifetime, and failure symptoms - #3065

Merged
bpamiri merged 2 commits into
developfrom
peter/docs-audit-request-lifecycle
Jun 12, 2026
Merged

docs(web/guides): correct request-lifecycle stage order, config() lifetime, and failure symptoms#3065
bpamiri merged 2 commits into
developfrom
peter/docs-audit-request-lifecycle

Conversation

@bpamiri

@bpamiri bpamiri commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

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

  1. Stages 1 and 2 swapped — dispatch/route match runs BEFORE middleware. $request() calls $paramParser()$findMatchingRoute() (vendor/wheels/Dispatch.cfc:382, :518; Wheels.RouteNotFound thrown at :263-270) before variables.$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.

  2. config() runs once per application lifetime, not per-request. config() runs inside $initControllerClass (vendor/wheels/Controller.cfc:59-63) and the class is cached in application.wheels.controllers (Global.cfc controller() ~1209-1230, cleared only on reload). Live probe: one CONFIG-RAN log 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.

  3. afterAction filters CAN replace the response body. A renderText() call in an afterAction filter replaced the action's body (HIJACKED instead of pong) on both engines. "The response body is already determined by this point" replaced with accurate wording + a warning against render calls in after-filters.

  4. 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 $callAction to auto-render and throws Wheels.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.

  5. Fabricated symptom removed: "Blank page but curl -I shows 200". A missing view is never a blank 200: $throwErrorOrShow404Page sets statusCode=404 before throwing (vendor/wheels/Global.cfc:2501-2510). Dev = full Wheels.ViewNotFound error page, production = generic 404 page; both engines, GET and HEAD. The all-lowercase path advice (verified correct) is kept.

  6. Nit: actual route-not-found dev message is "Could not find a route that matched this request." (Dispatch.cfc:270), type Wheels.RouteNotFound.

  7. 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).
  • All source line citations re-checked against current develop in this worktree.

No open-issue behavior (#3059#3063) intersects this page — it contains no reload, debug-bar, or injector() content.

🤖 Generated with Claude Code

…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>

@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 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() at vendor/wheels/Dispatch.cfc:382, which calls $findMatchingRoute() at :518; the middleware pipeline runs at :450. The OPTIONS preflight carve-out at :360-379 is 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-424 carries params and route (from request.wheels.currentRoute).
  • config() lifetimeconfig() is invoked inside class init (vendor/wheels/Controller.cfc:59-63) and the class is cached via $doubleCheckedLock in controller() (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 later renderText()/renderView() from overwriting the response. Consistent with the author's live probes.
  • Removed fabricated symptomsgrep -rn 'has no public method' vendor/wheels/ → 0 hits; the real behavior is Wheels.ViewNotFound ("Could not find the view page for the <action> action…", processing.cfc:212). And $throwErrorOrShow404Page sets $header(statusCode = 404) at Global.cfc:2502 before throwing/including, so a missing view is never a blank 200.
  • Nits verified — route-not-found message matches Dispatch.cfc:270; [key] segment syntax matches vendor/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 ($createControllerClass via $doubleCheckedLock, Global.cfc:1218-1224). A reader could infer all config() 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>

@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 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() at vendor/wheels/Dispatch.cfc:382$findMatchingRoute() at :518; the middleware pipeline runs at :450. The OPTIONS preflight short-circuit at Dispatch.cfc:344-379 is 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-424 carries params and route from request.wheels.currentRoute.
  • afterAction can replace the body — after-filters run via $runFilters(type="after", ...) at vendor/wheels/controller/processing.cfc:123, after $callAction at :113, with no guard against a later renderText()/renderView() overwriting the response.
  • Fabricated symptoms removedgrep -rn 'has no public method' vendor/wheels/ → 0 hits; the real missing-action behavior is Wheels.ViewNotFound ("Could not find the view page for the <action> action…", processing.cfc:212). And $header(statusCode = 404) at Global.cfc:2502 runs before throw/include, so a missing view is never a blank 200.
  • Nits verified — RouteNotFound message matches Dispatch.cfc:270; [key] segment syntax matches vendor/wheels/mapper/matching.cfc:190-198.
  • New Aside (#3061)public/Application.cfc:345 does unconditionally run application.wheelsdi = new wheels.Injector("wheels.Bindings") in onError(), and the issue is OPEN. The Aside's claim is accurate in substance (one scoping nit below).

Docs

  1. 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 reaches onError when showErrorInformation is true: in production $throwErrorOrShow404Page includes the static 404 page and aborts 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.

  2. request-lifecycle.mdx:70 and :99 — "once per application lifetime" precision (carried over from my review of 030dc29). config() runs lazily, once per controller class, on the first request that reaches that controller ($createControllerClass via $doubleCheckedLock, vendor/wheels/Global.cfc:1218-1224). "Once per application lifetime (on the first request after a start or reload)" could read as all config() 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 (onError in public/Application.cfc). ✓

Docs-only change: no changelog fragment or spec coverage required; no security or cross-engine surface.

@bpamiri
bpamiri merged commit e2862eb into develop Jun 12, 2026
14 checks passed
@bpamiri
bpamiri deleted the peter/docs-audit-request-lifecycle branch June 12, 2026 04:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant