Summary
Dispatch builds the middleware request context as {params, route, pathInfo, method} (vendor/wheels/Dispatch.cfc ~lines 420-425) — there is no cgi member. Two guides (and repo CLAUDE.md) document middleware code that reads req.cgi.* / request.cgi.*, which therefore always misses and silently degrades:
- Rate-limiting keyFunction (
digging-deeper/rate-limiting.mdx:131-145, same snippet in repo CLAUDE.md): req.cgi.http_x_api_key ?: "anonymous" Elvis-defaults on every live request, so ALL clients collapse into the single "anonymous" budget. Security-relevant for exactly the use case the section targets (token-authenticated APIs behind NAT). Verified live with maxRequests=3: 3× X-Api-Key: aaa → 200,200,200; then X-Api-Key: bbb → 429; no-header → 429.
- Observability InboundRequestId example (
deployment/observability-and-logging.mdx): request.cgi.http_x_request_id ?: "" is always empty, so curl -H 'X-Request-Id: test-123' gets a fresh UUID — the documented header passthrough never happens. (Adobe bonus trap: request.wheels.requestId = ... inside handle() writes to the shadowing request parameter, not the scope — see RequestId.cfc's own $writeRequestId comment.)
Cors.cfc already defends against this exact trap (StructKeyExists(arguments.request, "cgi") + engine-CGI fallback, lines 89-93/138-142), and RateLimiter.cfc:224-244 does the same for client IP — the framework's own middleware know the context has no cgi; the docs don't.
Verified working forms (live, Lucee 7, develop @ 840274b)
// keyFunction — note: bare `cgi.http_x_api_key ?: "anonymous"` is ALSO wrong,
// because a missing header reads as empty string, so the Elvis never fires.
keyFunction=function(req) {
var apiKey = cgi.http_x_api_key;
return Len(apiKey) ? apiKey : "anonymous";
}
→ aaa: 200,200,200,429 (own budget); bbb: 200; no header: 200 (separate anonymous bucket). The InboundRequestId example likewise works after switching to the real cgi scope (echoes test-123, still generates a UUID when absent).
The RateLimiter specs only pass hand-built request structs that DO carry cgi/remoteAddr, so no spec catches the live-dispatch shape.
Proposed direction (pick one)
- Framework parity (nicer): add
cgi (or a curated header subset) to the Dispatch middleware request context so the documented req.cgi.* form works and per-component engine-scope fallbacks (Cors, RateLimiter) become redundant. Docs still need the empty-string/Len() correction.
- Docs-only (minimal): replace both snippets (plus repo CLAUDE.md's keyFunction) with the
Len()-guarded bare-cgi form, and add a spec that drives RateLimiter through real Dispatch to pin the context shape.
While editing, correct the adjacent rationale at rate-limiting.mdx line 145: an empty-string key doesn't "accumulate counter entries" — it merges all header-less traffic into one shared budget (memory is separately bounded by maxStoreSize/maxKeyLength).
Acceptance
- Either the middleware context carries
cgi (with a spec) or all documented req.cgi middleware examples are replaced with verified-working forms (guide + CLAUDE.md).
- A dispatch-level spec exercises keyFunction with real headers so the context-shape regression class is pinned.
Reported by the guide-behavioral-audit P1 batch 2 (work items p1-11-ratelimit claim rl-14, p1-16-deploy claim obs-03 — two independent live repros, develop @ 840274b).
Summary
Dispatchbuilds the middleware request context as{params, route, pathInfo, method}(vendor/wheels/Dispatch.cfc~lines 420-425) — there is nocgimember. Two guides (and repo CLAUDE.md) document middleware code that readsreq.cgi.*/request.cgi.*, which therefore always misses and silently degrades:digging-deeper/rate-limiting.mdx:131-145, same snippet in repo CLAUDE.md):req.cgi.http_x_api_key ?: "anonymous"Elvis-defaults on every live request, so ALL clients collapse into the single"anonymous"budget. Security-relevant for exactly the use case the section targets (token-authenticated APIs behind NAT). Verified live withmaxRequests=3: 3×X-Api-Key: aaa→ 200,200,200; thenX-Api-Key: bbb→ 429; no-header → 429.deployment/observability-and-logging.mdx):request.cgi.http_x_request_id ?: ""is always empty, socurl -H 'X-Request-Id: test-123'gets a fresh UUID — the documented header passthrough never happens. (Adobe bonus trap:request.wheels.requestId = ...insidehandle()writes to the shadowingrequestparameter, not the scope — seeRequestId.cfc's own$writeRequestIdcomment.)Cors.cfcalready defends against this exact trap (StructKeyExists(arguments.request, "cgi")+ engine-CGI fallback, lines 89-93/138-142), andRateLimiter.cfc:224-244does the same for client IP — the framework's own middleware know the context has nocgi; the docs don't.Verified working forms (live, Lucee 7, develop @ 840274b)
// keyFunction — note: bare `cgi.http_x_api_key ?: "anonymous"` is ALSO wrong, // because a missing header reads as empty string, so the Elvis never fires. keyFunction=function(req) { var apiKey = cgi.http_x_api_key; return Len(apiKey) ? apiKey : "anonymous"; }→ aaa: 200,200,200,429 (own budget); bbb: 200; no header: 200 (separate anonymous bucket). The InboundRequestId example likewise works after switching to the real
cgiscope (echoestest-123, still generates a UUID when absent).The RateLimiter specs only pass hand-built request structs that DO carry
cgi/remoteAddr, so no spec catches the live-dispatch shape.Proposed direction (pick one)
cgi(or a curated header subset) to the Dispatch middleware request context so the documentedreq.cgi.*form works and per-component engine-scope fallbacks (Cors, RateLimiter) become redundant. Docs still need the empty-string/Len()correction.Len()-guarded bare-cgiform, and add a spec that drives RateLimiter through real Dispatch to pin the context shape.While editing, correct the adjacent rationale at rate-limiting.mdx line 145: an empty-string key doesn't "accumulate counter entries" — it merges all header-less traffic into one shared budget (memory is separately bounded by maxStoreSize/maxKeyLength).
Acceptance
cgi(with a spec) or all documentedreq.cgimiddleware examples are replaced with verified-working forms (guide + CLAUDE.md).Reported by the guide-behavioral-audit P1 batch 2 (work items p1-11-ratelimit claim rl-14, p1-16-deploy claim obs-03 — two independent live repros, develop @ 840274b).