Skip to content

fix cache : replay a response that carries Vary only for the matching request headers - #8079

Merged
Aias00 merged 1 commit into
apache:masterfrom
cloudbase-ai:fix/cache-vary-selection
Oct 24, 2026
Merged

Aias00 merged 1 commit into
apache:masterfrom
cloudbase-ai:fix/cache-vary-selection

Conversation

@cloudbase-ai

@cloudbase-ai cloudbase-ai commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem

CachedResponse.capture refuses to store any response that carries a Vary header:

|| headers.containsKey(HttpHeaders.VARY)   // -> Optional.empty(), no log

The rejection was added in #7662; since #7901 (CrossFilter) adds Vary: Origin to every gateway response, including non-CORS ones, the cache plugin now stores nothing at all. CachePluginTest in shenyu-integrated-test-http fails on any branch containing both changes (testMemoryModle / testRedisModle: expected: <true> but was: <false>), because the second request is no longer served from the cache.

A response that declares Vary is not uncacheable — it is only reusable for requests that carry the same values for the listed fields. Rejecting it outright trades a correctness rule for a total loss of caching.

Change

  • CachedResponse stores the request header values selected by Vary (selectingValues) next to the response, and matches(request) compares them on lookup. A hit is replayed only when every listed field still has the same values; otherwise the request is a miss and goes upstream.
  • Vary: * is still never stored, since no request can select it.
  • CachePlugin applies the same check on the lookup path and on the single-flight follow path, so a shared fill is never handed to a request that selects another variant.
  • Decoding validates the envelope: an entry must select exactly the fields its own Vary header lists, and the stored values must be valid header values. Entries written before this change (no selectingValues) still decode, so a rolling upgrade does not invalidate the existing cache.
  • capture now receives the request in addition to the response.

Trade-off worth calling out

The response key is still per (uri, selector, rule), so a rule serving many Origin variants overwrites the entry instead of keeping one entry per variant. Correctness is preserved — a wrong variant is never replayed — only the hit rate suffers for those rules. Keying per variant would require knowing the variant list before the first response is seen; happy to follow up with that if you prefer.

Tests

mvn -pl shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-handler test — 100 tests green, checkstyle clean.

  • CacheResponseReplayTest#testVaryHeaderAddedBeforeCommitStillAllowsCaching reproduces the reported failure: a Vary: Origin added in beforeCommit (exactly what CrossFilter does) no longer prevents caching.
  • CacheResponseReplayTest#testResponseVaryingOnRequestHeaderIsReusedOnlyForTheSameVariant: the same Origin hits, another Origin misses and receives its own body.
  • CachedResponseTest: Vary: * is not stored, selectingValues must agree with the Vary header, legacy entries still decode.

Both new replay tests fail without this change (upstream calls 2 instead of 1, and 3 instead of 2).

@cloudbase-ai

cloudbase-ai commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Correction to my earlier note — I had the mechanism wrong.

The it workflow's path filter does include shenyu-plugin/** (via shenyu-*/**). What skips the job is .github/scripts/resolve-test-case-matrix.sh, which maps changed files to integrated-test cases. In mode=integration, shenyu-plugin/** only maps to a case when the path is shenyu-plugin/pom.xml, plugin-api, plugin-base, or contains dubbo|grpc|websocket|http|divide|springmvc. A shenyu-plugin-cache change matches nothing, so run_integration=false and every build (shenyu-integrated-test-*) job is skipped. That is why shenyu-integrated-test-http does not run on this PR.

Worth recording while we are here: the PR that introduced the regression (#7901) touched shenyu-web/**, which sets full_required=true, so all 11 cases ran and build (shenyu-integrated-test-http) failed on 2026-10-21 with exactly this test — CachePluginTest.testMemoryModle:54 and testRedisModle:73, expected: <true> but was: <false> — and the PR was merged on 2026-10-23 anyway. Master's push run for 34f2ed0 shows the same red. So the gate did its job; the merge went through red CI.

Verification for this PR is therefore local:

  • mvn -pl shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-handler test → 100 tests green, checkstyle clean.
  • With the main-code change reverted, the two new replay tests fail exactly like the integrated test does: expected: <1> but was: <2> for a Vary: Origin added in beforeCommit, and expected: <2> but was: <3> for the origin-variant case.

A follow-up that would actually prevent this class of regression: map shenyu-plugin/** (at least shenyu-plugin-cache) to shenyu-integrated-test-http in resolve-test-case-matrix.sh. Happy to send that separately.

@cloudbase-ai
cloudbase-ai force-pushed the fix/cache-vary-selection branch from 3b856b2 to a66575d Compare October 24, 2026 00:07
@cloudbase-ai

Copy link
Copy Markdown
Contributor Author

Strengthened one test after re-reviewing it: testVaryHeaderAddedBeforeCommitStillAllowsCaching fired the same request twice, which passes whether or not Vary is visible at capture time — so it did not actually pin the ordering it claims to cover. It now re-requests with a different Origin and asserts a miss (calls == 2); that only holds if CachedResponse.capture really sees the header added in beforeCommit.

That ordering is the reason this fix works against #7901: CacheHttpResponse.writeBody registers the capture action after the handler (and after any WebFilter such as CrossFilter) has registered its own beforeCommit, so response headers are complete when the selecting values are recorded.

Also worth stating explicitly, since it is a trade-off rather than an oversight: the cache key is still (uri, selectorId, ruleId), so one rule serving several Origin values overwrites its own entry and the hit rate drops to zero for that mix. Correctness is preserved (a variant is never replayed to another one), and making the key variant-aware is a separate change.

@cloudbase-ai

Copy link
Copy Markdown
Contributor Author

Follow-up opened: #8081 makes shenyu-plugin/** changes that name no protocol keyword select shenyu-integrated-test-http, so a change like this one would actually be exercised before merge. Until that lands, the local runs described above remain the only coverage for this PR.

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@/tmp/rv8079/review_8079.md

@Aias00
Aias00 merged commit 86e932c into apache:master Oct 24, 2026
28 checks passed
dmsolr pushed a commit that referenced this pull request Oct 25, 2026
Sync to current master to resolve the stable shenyu-integrated-test-http CI failure (CachePluginTest.testMemoryModle/testRedisModle). Root cause: this branch predated #8079, whose pre-fix CachedResponse.capture() refused to store any response carrying a Vary header, so the cached endpoint was never reused and both requests hit the backend. #8079 keys Vary responses on the selecting request headers instead. Also pulls in the divide/loadbalancer work (#8076, #7172, #7986, #8005, #7821, #8033, #8090).

#7172 removeStaleUpstreams reconciles via removeFromMap/triggerRemoveOne, which republish the immutable healthy snapshot under the same lock, so the read-only snapshot model stays correct with no stale retained instances.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants