Skip to content

fix(controller): harden $callAction ViewNotFound and $useLayout wipe (B5–B6) - #3401

Merged
bpamiri merged 5 commits into
developfrom
cursor/controller-hardener-b5-shoulds-27c5
Aug 24, 2026
Merged

fix(controller): harden $callAction ViewNotFound and $useLayout wipe (B5–B6)#3401
bpamiri merged 5 commits into
developfrom
cursor/controller-hardener-b5-shoulds-27c5

Conversation

@bpamiri

@bpamiri bpamiri commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Controller / request-lifecycle hardener slice on top of #3400 (B1–B4, B7–B8 already on develop). Draft for Lead/CoS review.

BLOCKERs

  • B5$callAction auto-render catch mapped any exception to Wheels.ViewNotFound when action.cfm was missing, masking layout/render errors. Now only genuine missing-view includes ($isMissingViewException + missing file) become typed Wheels.ViewNotFound; other types are rethrown.
  • B6$useLayout reset rv = useDefault on every loop pass, so a later non-matching usesLayout wiped a prior match (silent layout bypass). Once a match is chosen, later non-matches leave it alone; a later match still overrides.

SHOULDs touched

  • $callActionAndAddToCache — does not store a cache entry for a redirect-only action (empty body / blank 200 on hit).
  • filterChain("all") — returns a Duplicate() copy so callers cannot mutate the live $class.filters array.
  • processAction halt — returns false when a verification aborts or a before filter returns false. Additive: success path is still true. Callers that ignore the return are unchanged.
  • $findRoute multi-name — no longer fail-opens to the last same-named candidate. Selects the first candidate matching method (when supplied) and required variables; throws Wheels.RouteNotFound when none match.
  • processRequest CSRF ignoredefault stays ignore (historic test helper). Opt-in csrf="exception" / csrf="abort" enforces. Production protectsFromForgery() default is unchanged.
  • filters(placement="prepend") + multi-througha,b,c stays a,b,c in front of the existing chain (was reversed to c,b,a).
  • redirectTo(url=) params — encoded via $constructParams like back=true.
  • Blank layout-function return — treated as useDefault (matches the in-source contract / docs), not as “no layout”.

renderingSpec specified_layouts examples now clear dummy class layouts so they test one declaration at a time and no longer depend on the B6 wipe.

Lead accepted (a) on the three public flips ($findRoute RouteNotFound, redirectTo(url=) encode, blank layout-fn → default). Those stay as shipped.

B5 — why 974b3645e was UNPROVEN on LuCLI, and what 2f440a27e changes

LuCLI 32721709696: both B5 its did not throw. The showErrorInformation=true pin was the wrong lever.

Root cause (full-suite only): $callAction skips auto-render when $requestContentType() is not html and the format is not in $acceptableFormats. Earlier bundles (providesSpec) write request.cgi.http_accept to application/json / application/pdf on the shared request.cgi struct ($$oldCGIScope = request.cgi is a reference, not a copy). After those specs, Accept is leftover non-html, auto-render never runs, $useLayout / exploding layout never run, both its finish cleanly. Isolated filter=hardener never loads providesSpec, so it stayed green.

$throwErrorOrShow404Page abort was a second swallow if the missing-view path was reached with showErrorInformation off.

Production change (flagged — not a public HTTP 404 flip):

  • $callAction now always Throws typed Wheels.ViewNotFound for a genuine missing view ($header(404) then Throw). It no longer include+aborts via $throwErrorOrShow404Page.
  • processAction (HTTP dispatch) catches Wheels.ViewNotFound and still calls $throwErrorOrShow404Page, so apps still get HTTP 404 + onmissingtemplate when showErrorInformation is off.
  • Layout / other render errors still Throw(object = e) with their original type.

Test change: B5 pins params.format=html + request.cgi.http_accept=text/html, and sets showErrorInformation=false via set() so the typed Throw is proven — not the old throw-path pin.

PROVEN / UNPROVEN

Item Status Filter evidence
B5 ViewNotFound swallow PROVEN wheels test --core --ci --filter=hardener → 41 passed (showErrorInformation=false)
B6 $useLayout wipe PROVEN same
Redirect-only action cache PROVEN same (asserts no cache key written)
filterChain("all") copy PROVEN same
processAction halt return PROVEN same
$findRoute multi-name fail-open PROVEN same — Lead accepted (a)
processRequest CSRF default flip UNPROVEN / escalate Default remains ignore. Do not merge a silent flip.
processRequest CSRF opt-in csrf="exception" PROVEN same
prepend multi-through order PROVEN same
redirectTo(url=) encode PROVEN same (%20 or +) — Lead accepted (a)
Blank layout-fn return PROVEN same — Lead accepted (a)
Missing super.config() CSRF UNPROVEN Already warn-only in development; fail-closed would be a default flip
URL scope wins over form UNPROVEN Documented contract in $mergeUrlAndFormScopes
CSRF header only copied on Ajax UNPROVEN Intentional form-field vs X-CSRF-Token header split
verifies handler fallthrough UNPROVEN (already on develop) $runVerifications already redirectTo(back=true)
$currentUserForPolicy catch → guest UNPROVEN (already on develop) Catch already falls through to guest
Silent double-render / redirect after render UNPROVEN Last-write-wins is current public API; throwing would be a default flip
onlyProvides unacceptable format → HTML UNPROVEN Documented 4.0.4 fallback
CSRF before before-filters UNPROVEN Reordering is a default flip; CSRF-first is fail-closed
Non-HTML auto-render skip → empty 200 UNPROVEN 406 would be a default flip
only+except OR semantics UNPROVEN Documented OR; AND would be a default flip
Filter type case-sensitivity skipped Already B8 on develop

Default-flip flags (Lead / CoS)

  • processRequest CSRF with="ignore" default: DO NOT MERGE a silent flip. Opt-in csrf="exception" only. Production protectsFromForgery() remains exception.
  • processAction returning false on halt is additive (was always true).
  • $findRoute throwing RouteNotFound instead of returning the last same-named candidate is fail-closed. Lead accepted (a).
  • $callAction ViewNotFound is now always a typed Throw. HTTP 404 via processAction is unchanged. Direct $callAction callers (tests / custom dispatch) no longer see include+abort when showErrorInformation is off.

HEAD

2f440a27e1f96069c527d67f4637177162d567b6

Test filters

wheels test --core --ci --filter=hardener
wheels test --core --ci --filter=controller

Green counts (this HEAD)

Filter Result
hardener 41 passed (B5 its included; showErrorInformation=false)
controller 550 passed (processAction catch path)

Related

Follow-on to #3400. Does not re-do B1–B4 / B7–B8.

Type of Change

  • Bug fix
  • Enhancement to existing feature

Feature Completeness Checklist

  • DCO sign-off
  • Tests
  • Framework Docs — no public default change; processRequest(csrf=) is test-helper opt-in
  • AI Reference Docs
  • CLAUDE.md — no convention change
  • Changelog fragmentchangelog.d/controller-hardener-b5-b6.fixed.md, changelog.d/controller-hardener-shoulds.changed.md
  • Test runner passes — scoped hardener + controller on this HEAD
Open in Web Open in Cursor 

Adversarial specs for $callAction ViewNotFound masking, $useLayout
later-match wipe, redirect-only action cache, live filterChain("all"),
processAction halt return, $findRoute multi-name fail-open, processRequest
CSRF ignore, prepend through-order, redirectTo(url=) encoding, and blank
layout-function returns.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>
Map ViewNotFound only for genuine missing-view includes, stop later
non-matching usesLayout from wiping a prior match, skip caching
redirect-only action bodies, copy filterChain(), return processAction
halt, fail closed on unmatched same-named $findRoute, keep prepend
through-order, encode redirectTo(url=) params, and treat a blank
layout-function return as the default layout. processRequest CSRF
ignore stays the default; csrf=exception is opt-in.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>
@github-actions github-actions Bot added the docs label Aug 24, 2026
cursoragent and others added 3 commits August 24, 2026 11:18
Clear dummy class layouts between specified_layouts examples so they
do not depend on the $useLayout wipe. Accept EncodeForURL space as
either %20 or +.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>
$throwErrorOrShow404Page aborts without a typed exception when
showErrorInformation is off, so leftover CI suite state left
thrown.type empty on ViewNotFound and HardenerLayoutError.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>
Genuine missing views now Throw Wheels.ViewNotFound instead of
$throwErrorOrShow404Page include+abort. processAction still presents
the production 404 page so HTTP 404 for apps is unchanged.

B5 specs pin params.format=html (leftover Accept skipped auto-render)
and set showErrorInformation=false so the typed throw is proven.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Peter Amiri <peter@alurium.com>
@bpamiri
bpamiri marked this pull request as ready for review August 24, 2026 11:40
@bpamiri
bpamiri merged commit 324efe2 into develop Aug 24, 2026
13 checks passed
@bpamiri
bpamiri deleted the cursor/controller-hardener-b5-shoulds-27c5 branch August 24, 2026 11:40
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.

2 participants