Summary
.scope() does not accept a callback parameter (vendor/wheels/mapper/scoping.cfc:17-29), and namespace()/package()/controller() just forward to it. Only group() (scoping.cfc:166-192, executes the callback then auto-end()s — inherited by api()/version()) and resources() consume callbacks. CFML happily accepts the extra named argument, so the documented callback forms fail twice, both silently:
- The callback never executes — routes declared inside it simply don't exist (404
Wheels.RouteNotFound).
- The scope is never closed, so every subsequent route (resources, wildcard, even root) inherits the scope's path prefix AND middleware. The misrouted URLs even respond (e.g.
/admin/posts 200 because the outer .resources("posts") got swallowed into the open namespace), which makes the missing public routes very hard to diagnose.
This is the single most-repeated broken pattern found by the batch-2 guide audit — three independent verifiers hit it from three different guides.
Repro (develop @ 840274b, Lucee 7 — mapper logic is engine-independent)
Rate-limiting guide shape:
mapper()
.scope(
path="/limited",
middleware=[new wheels.middleware.RateLimiter(maxRequests=2, windowSeconds=15)],
callback=function(map) {
map.get(name="limitedIndex", pattern="/", to="limited##index");
}
)
.wildcard()
.root(method="get")
.end();
Observed: limitedIndex never created; .wildcard()/.root() nested under /limited (GET /limited/probe → 200 via the nested wildcard, GET /probe → 404). The whole app's top-level routing silently breaks.
Routing-guide shape (.namespace(name="admin", callback=...) followed by an outer .resources("posts")): /admin/posts → 200 only because the open namespace swallowed the outer resources; public /posts → 404. The manual .namespace("admin")...end() form works correctly (both URL families 200). Framework specs only ever use the manual form (vendor/wheels/tests/specs/mapperSpec.cfc:165).
CORS-guide shape (.scope(path="/api", middleware=[Cors], callback=...)): identical — /api/ping 404, /plain-ping 404, /api/plain-ping 200 with the leaked scoped CORS headers.
Root cause
scope() declares no callback argument; the extra named arg lands in arguments, is prepended onto the scope stack (scoping.cfc:91), and is never invoked — and nothing pops the stack. scoping.cfc's own forwarding comments say options should "not be silently dropped"; callback currently is, and worse, dropping it changes the meaning of everything declared after it.
Documented call sites teaching the broken form (all shipped since the v4 guides rewrite #2169)
web/sites/guides/src/content/docs/v4-0-0/digging-deeper/rate-limiting.mdx lines 183-201
web/sites/guides/src/content/docs/v4-0-0/digging-deeper/cors.mdx lines 94-114
web/sites/guides/src/content/docs/v4-0-0/digging-deeper/multi-tenancy.mdx line 271
web/sites/guides/src/content/docs/v4-0-0/digging-deeper/route-model-binding.mdx line 118
web/sites/guides/src/content/docs/v4-0-0/basics/routing.mdx "Namespaced sections" (.namespace(name="admin", callback=...))
- repo
CLAUDE.md Route Model Binding section (.scope(path="/api", binding=true) is fine, but check for callback forms)
Proposed direction
Preferred: implement callback= in scope() mirroring group() — exclude callback from the scope-stack entry, execute arguments.callback(this) after the prepend, then auto-end() (~6 lines, scoping.cfc:185-189 is the template). That makes namespace(), package(), and controller() consistent with group()/resources() and fixes every guide example as written.
Fallback: throw Wheels.InvalidArgument in development when scope() receives an unconsumed callback, and rewrite the five guide call sites to the explicit .end() (or .group()) form. Either way, silent acceptance is the worst of the three options.
Also worth fixing alongside: namespaced controllers under app/controllers/admin/ need extends="app.controllers.Controller" — bare extends="Controller" throws "invalid component definition" on Lucee from the subfolder; no guide mentions this.
Acceptance
.scope(path="/x", callback=function(map){ map.get(...); }) registers the callback routes and auto-closes the scope (or throws loudly in development), with specs covering scope/namespace/package callback forms and the no-swallow guarantee for routes declared after.
- The five guide examples above work as written (or are rewritten in the same PR if the throw option is chosen).
Reported by the guide-behavioral-audit P1 batch 2 (work items p1-10-routing claims R11/R12, p1-11-ratelimit claim rl-22, p1-12-cors claim cors-17 — three independent live repros, develop @ 840274b).
Summary
.scope()does not accept acallbackparameter (vendor/wheels/mapper/scoping.cfc:17-29), andnamespace()/package()/controller()just forward to it. Onlygroup()(scoping.cfc:166-192, executes the callback then auto-end()s — inherited byapi()/version()) andresources()consume callbacks. CFML happily accepts the extra named argument, so the documented callback forms fail twice, both silently:Wheels.RouteNotFound)./admin/posts200 because the outer.resources("posts")got swallowed into the open namespace), which makes the missing public routes very hard to diagnose.This is the single most-repeated broken pattern found by the batch-2 guide audit — three independent verifiers hit it from three different guides.
Repro (develop @ 840274b, Lucee 7 — mapper logic is engine-independent)
Rate-limiting guide shape:
mapper() .scope( path="/limited", middleware=[new wheels.middleware.RateLimiter(maxRequests=2, windowSeconds=15)], callback=function(map) { map.get(name="limitedIndex", pattern="/", to="limited##index"); } ) .wildcard() .root(method="get") .end();Observed:
limitedIndexnever created;.wildcard()/.root()nested under/limited(GET /limited/probe→ 200 via the nested wildcard,GET /probe→ 404). The whole app's top-level routing silently breaks.Routing-guide shape (
.namespace(name="admin", callback=...)followed by an outer.resources("posts")):/admin/posts→ 200 only because the open namespace swallowed the outer resources; public/posts→ 404. The manual.namespace("admin")...end()form works correctly (both URL families 200). Framework specs only ever use the manual form (vendor/wheels/tests/specs/mapperSpec.cfc:165).CORS-guide shape (
.scope(path="/api", middleware=[Cors], callback=...)): identical —/api/ping404,/plain-ping404,/api/plain-ping200 with the leaked scoped CORS headers.Root cause
scope()declares nocallbackargument; the extra named arg lands inarguments, is prepended onto the scope stack (scoping.cfc:91), and is never invoked — and nothing pops the stack. scoping.cfc's own forwarding comments say options should "not be silently dropped";callbackcurrently is, and worse, dropping it changes the meaning of everything declared after it.Documented call sites teaching the broken form (all shipped since the v4 guides rewrite #2169)
web/sites/guides/src/content/docs/v4-0-0/digging-deeper/rate-limiting.mdxlines 183-201web/sites/guides/src/content/docs/v4-0-0/digging-deeper/cors.mdxlines 94-114web/sites/guides/src/content/docs/v4-0-0/digging-deeper/multi-tenancy.mdxline 271web/sites/guides/src/content/docs/v4-0-0/digging-deeper/route-model-binding.mdxline 118web/sites/guides/src/content/docs/v4-0-0/basics/routing.mdx"Namespaced sections" (.namespace(name="admin", callback=...))CLAUDE.mdRoute Model Binding section (.scope(path="/api", binding=true)is fine, but check for callback forms)Proposed direction
Preferred: implement
callback=inscope()mirroringgroup()— excludecallbackfrom the scope-stack entry, executearguments.callback(this)after the prepend, then auto-end()(~6 lines, scoping.cfc:185-189 is the template). That makesnamespace(),package(), andcontroller()consistent withgroup()/resources()and fixes every guide example as written.Fallback: throw
Wheels.InvalidArgumentin development whenscope()receives an unconsumedcallback, and rewrite the five guide call sites to the explicit.end()(or.group()) form. Either way, silent acceptance is the worst of the three options.Also worth fixing alongside: namespaced controllers under
app/controllers/admin/needextends="app.controllers.Controller"— bareextends="Controller"throws "invalid component definition" on Lucee from the subfolder; no guide mentions this.Acceptance
.scope(path="/x", callback=function(map){ map.get(...); })registers the callback routes and auto-closes the scope (or throws loudly in development), with specs covering scope/namespace/package callback forms and the no-swallow guarantee for routes declared after.Reported by the guide-behavioral-audit P1 batch 2 (work items p1-10-routing claims R11/R12, p1-11-ratelimit claim rl-22, p1-12-cors claim cors-17 — three independent live repros, develop @ 840274b).