feat: route-level cache rules via Router.withCache() - #682
Merged
lmajano merged 4 commits intoAug 17, 2026
Conversation
Grounded pass through system/ to evaluate which ideas from Express 5 (middleware chains) and Nuxt 4/Nitro (layers, route rules, DevTools) are worth adopting. Confirms route-scoped middleware, HTTP caching primitives, generalized SSE, and AI conversational context already close what were previously the sharpest gaps. Narrows remaining recommendations to route-level cache rules, a first-party introspection/DevTools surface, and app-level config layers - each cited against actual file/line sources. Analysis document only. No framework code changes.
Adds a route-scoped alternative to handler cache="true" annotations, matching Nitro's routeRules idea from the Nuxt/Express analysis. A route that calls .withCache() gets cache, cacheTimeout, cacheLastAccessTimeout, cacheProvider, cacheSuffix, cacheInclude, cacheExclude, cacheFilter, and the Tier 1 HTTP caching flags (etag, etagWeak, lastModified, cacheControl) on its route record - the same set a handler annotation already supports, one-for-one. HandlerService.getRouteCachingMetadata() reads the matched route's record and, when it declares cache=true, takes full precedence over that handler's own annotations for the request - checked first by both the pre-execution cache lookup (getEventMetadataEntry()) and the post-execution cache write (getEventCachingMetadata()). Deliberately not memoized like the handler- annotation dictionary: a route record is already a cheap struct read, and recomputing it fresh per request is what lets two different routes to the same event carry two different cache policies, which the event-name-keyed handler dictionary could never do. Falls through unchanged to the existing handler-annotation path when a route doesn't opt in - zero behavior change for existing apps. Rides the same EventURLFacade/CacheBox/Bootstrap.cfc plumbing a cache="true" annotation already uses, so no other file needed to change. Updates the Nuxt/Express analysis doc to mark this recommendation shipped.
… return private struct function getRouteCachingMetadata() returned a bare `return;` (null) for the common "route doesn't opt into caching" case. Lucee enforces the declared struct return type strictly and throws UDFCasterException: Cannot cast null value to value of type [struct] on every request, which is why CI failed with 62 TestBox failures on lucee@5/lucee@6 - any spec touching HandlerService (event execution, handler bean lookups, etc) exercises getEventMetadataEntry(), which now always calls this function. Drop the explicit struct return type, matching the same "function that may return null" convention RequestService.cfc's getContextFromScope() already uses elsewhere in this codebase. Runtime behavior is unchanged - callers already null-check with isNull().
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces route-level caching declarations via Router.withCache(), allowing cache policy to live alongside URL definitions while reusing ColdBox’s existing Event Caching + Tier 1 HTTP caching plumbing.
Changes:
- Added
Router.withCache()and expanded the route record shape with cache/Tier-1 HTTP caching keys (etag,lastModified,cacheControl, etc.). - Updated
HandlerServiceto prefer route-derived caching metadata (from the matched route record) over handler action cache annotations for the current request. - Added/updated specs covering route cache defaults, stored values, closure suffix behavior, and precedence.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/specs/web/services/HandlerServiceTest.cfc | Adds integration coverage for route-driven caching metadata and precedence in HandlerService. |
| tests/specs/web/routing/RouterTest.cfc | Adds router specs verifying defaults and withCache() persistence on the route record. |
| system/web/services/HandlerService.cfc | Implements getRouteCachingMetadata() and consults it from both read/write caching metadata paths. |
| system/web/routing/Router.cfc | Adds route-record cache fields and implements the fluent withCache() modifier. |
| docs/analysis/nuxt-express-coldbox-analysis.md | Adds a source-grounded analysis document describing motivations and related framework comparisons. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+207
to
+224
| function buildRouteRecord( struct overrides = {} ){ | ||
| var base = { | ||
| "cache" : true, | ||
| "cacheTimeout" : 60, | ||
| "cacheLastAccessTimeout" : "", | ||
| "cacheProvider" : "template", | ||
| "cacheSuffix" : "", | ||
| "cacheInclude" : "*", | ||
| "cacheExclude" : "", | ||
| "cacheFilter" : "", | ||
| "etag" : false, | ||
| "etagWeak" : false, | ||
| "lastModified" : false, | ||
| "cacheControl" : "" | ||
| }; | ||
| base.append( arguments.overrides, true ); | ||
| return base; | ||
| } |
buildRouteRecord() was declared as a named function nested inside a describe() closure. This spec suite's convention (and TestBox specs generally) is to assign test helpers to a local var closure instead, sidestepping any engine differences around nested named function declarations. No behavior change - same signature, same call sites.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds a route-scoped alternative to handler
cache="true"annotations, so caching can be declared where the URL is declared instead of being buried on the handler action.Router.cfcgains a fluent.withCache()modifier and 12 new route-struct keys, mirroring every existing handler-level cache annotation one-for-one:cache,cacheTimeout,cacheLastAccessTimeout,cacheProvidercacheSuffix,cacheInclude,cacheExclude,cacheFilteretag,etagWeak,lastModified,cacheControl(Tier 1 HTTP caching)Precedence
A route that calls
.withCache()takes full precedence over that same event's handler-level cache annotations for any request matching it.HandlerService.getRouteCachingMetadata()reads the matched route's record and is consulted first by both the pre-execution cache lookup (getEventMetadataEntry()) and the post-execution cache write (getEventCachingMetadata()).It is deliberately not memoized the way the handler-annotation dictionary is - a route record is already a cheap struct read, and recomputing it fresh per request is what lets two different routes to the same event carry two different cache policies, which the event-name-keyed handler dictionary could never do.
Routes that don't opt in fall through unchanged to the existing handler-annotation path - zero behavior change for existing apps.
Implementation notes
EventURLFacade/CacheBox/Bootstrap.cfccaching plumbing - no new subsystem, no other runtime files needed to change.cacheSuffixclosures use signaturefunction( event ), distinct from the handler-levelEVENT_CACHE_SUFFIX'sfunction( eventHandlerBean, event ), since a route has no reflected handler action metadata to hand it.docs/analysis/) that this feature was scoped from, examining ColdBox's routing/interception composition model against its own source.Jira Issues
COLDBOX-1418
Type of change
Checklist
Generated by Claude Code