Skip to content

Fix: LocalResponseCacheGatewayFilterFactory recreates Caffeine cache on every route refresh - #4244

Open
renechoi wants to merge 4 commits into
spring-cloud:mainfrom
renechoi:fix-4243
Open

Fix: LocalResponseCacheGatewayFilterFactory recreates Caffeine cache on every route refresh#4244
renechoi wants to merge 4 commits into
spring-cloud:mainfrom
renechoi:fix-4243

Conversation

@renechoi

@renechoi renechoi commented Jul 24, 2026

Copy link
Copy Markdown

Fixes #4243.

LocalResponseCacheGatewayFilterFactory built a new Caffeine cache and registered it in the CaffeineCacheManager under the same name every time the filter was applied. A route refresh applies the filter again, so the entries cached so far were dropped on every refresh.

This change reuses the cache already registered for the route, so cached entries survive a refresh. A new cache is still built in two cases:

  • the route cache configuration changed, so the previous cache would keep the old time to live or size;
  • the route has no id, because the cache name is then null-cache for every such route and cannot tell one route from another. Routes built with RouteLocatorBuilder are in this group, since GatewayFilterSpec.localResponseCache does not set a route id. Keeping the previous behaviour there avoids routes with different settings sharing one cache.

Verified against spring-cloud-gateway-server-webflux: the added unit tests fail without the change and pass with it, and the existing LocalResponseCacheGatewayFilterFactoryTests stay green, including shouldNotCacheResponseWhenTimeToLiveIsReached which relies on each route keeping its own time to live.

renechoi and others added 3 commits July 26, 2026 12:58
…on every route refresh

Signed-off-by: renechoi <renechoi90@gmail.com>
…s or route has no id

Signed-off-by: renechoi <renechoi90@gmail.com>

@anshulgoel22 anshulgoel22 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing #4243 — recreating the Caffeine cache on every route refresh defeats the purpose of local response caching in discovery-driven deployments.

What looks good

  • Reusing the registered cache when route cache configuration is unchanged preserves cached entries across routine refreshes (e.g. Eureka DiscoveryClient-CacheRefreshExecutor).
  • Rebuilding when TTL/size changes is the right trade-off: stale cache semantics are worse than a cold cache.
  • Handling routeId == null separately avoids different routes sharing one cache via null-cache — good edge-case callout.
  • Dedicated unit tests for reuse vs config-change vs null route id strengthen the fix.

Suggestion (non-blocking)
If feasible, an integration test that simulates two consecutive route refreshes with identical cache config and asserts a cache hit on the second request would complement the unit coverage — similar to the production scenario in the issue.

Nice, focused fix.

Signed-off-by: renechoi <renechoi90@gmail.com>
@renechoi

Copy link
Copy Markdown
Author

Thanks for the review, and for the integration test suggestion — added in 17b6177.

shouldServeCachedResponseAfterRouteRefreshWhenCacheConfigurationIsUnchanged warms the cache with one request, fires two consecutive RefreshRoutesEvents (awaiting the RefreshRoutesResultEvent each time, so the refresh has actually completed before the next request), then asserts the second request is still served from the cache that was built before the refreshes.

One detail worth flagging, because it decided how the test had to be written: its route is declared as a RouteDefinition rather than through RouteLocatorBuilder. GatewayFilterSpec.localResponseCache calls the apply(Consumer) overload, which never sets the route id on the config, so DSL-built routes always land on the routeId == null branch and rebuild their cache regardless. Only route definitions carry the id down to the filter configuration (RouteDefinitionRouteLocator sets it), which is also the shape a discovery-driven deployment has — so that is the only place the reuse behaviour is observable end to end.

I also checked that the test pins the fix rather than merely passing: reverting LocalResponseCacheGatewayFilterFactory to main while keeping the test makes it fail on all three attempts with JSON path "$.headers.X-Custom-Header" expected:<1> but was:<2>, i.e. the refresh discarded the cached response. With the fix it passes.

Local run: the spring-cloud-gateway-server-webflux suite is 847 tests, 0 failures. The 6 errors are Testcontainers cases that require Docker, which is unavailable on this machine, and they fail identically on an unmodified checkout. spring-javaformat:validate and checkstyle are clean.

@anshulgoel22

Copy link
Copy Markdown

Thanks for adding the integration test — the RefreshRoutesEvent sequencing and the revert-to-main failure check make this much stronger. Good callout on RouteDefinition vs RouteLocatorBuilder for observing reuse end-to-end.

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.

LocalResponseCacheGatewayFilterFactory recreates Caffeine cache on every route refresh

4 participants