fix(response_cache): entity fields returning null with cache-control: no-cache#9197
fix(response_cache): entity fields returning null with cache-control: no-cache#9197OriginLeon wants to merge 4 commits intodevfrom
Conversation
… no-cache is set When no-cache was present, cache_lookup_entities returned early with an empty IntermediateResult list. insert_entities_in_result iterates over that list to assemble the final response, so an empty list caused _entities to be [] and all entity fields to be null. Fix by treating no-cache as all-cache-miss instead of early returning, so that insert_entities_in_result receives a properly sized result list and can assemble the response correctly. Also avoids the unnecessary Redis round-trip when no-cache is set by moving fetch_multiple inside the else branch. Fixes ROUTER-1689 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
✅ Docs preview has no changesThe preview was not built because there were no changes. Build ID: aad5f5caa717d3799eb3d75b ✅ AI Style Review — No Changes DetectedNo MDX files were changed in this pull request. Review Log: View detailed log
|
|
@OriginLeon, please consider creating a changeset entry in |
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
carodewig
left a comment
There was a problem hiding this comment.
Great find, appreciate you fixing this!
I think the same issue will apply to the entity cache, so the fix should be made there as well.
| let cache_result: Vec<Option<CacheEntry>> = if cache_control.is_some_and(|c| c.is_no_cache()) { | ||
| std::iter::repeat_n(None, keys_len).collect() |
There was a problem hiding this comment.
The downside of this approach is that things that were never intended to hit the cache (ie requests with no-cache) will end up incrementing cache-miss metrics.
I think the easiest way to fix this is to add a record_metrics: bool argument to filter_representations and use it to skip the metrics insertion in that function (let _ = context.insert(CacheMetricContextKey::new(subgraph_name.to_string()), CacheSubgraph(cache_hit));)
Summary
Fixes ROUTER-1689 (escalation TSH-22520).
When
response_cacheis enabled and a request carriescache-control: no-cache, entity fields resolved via_entitiesqueries (non-owning subgraphs) were returningnull. Fields owned by the root subgraph were unaffected. Regression introduced in v2.13.0 via PR #8948.Root cause:
cache_lookup_entitieshad an early return whenno-cachewas detected, returningResponseCacheResults::default()— an emptyVec<IntermediateResult>. The downstream functioninsert_entities_in_resultiterates over that list to assemble the final response in order, interleaving cache hits with fresh subgraph data. With an empty list the loop never executed,new_entitiesstayed empty, and_entities: []was written into the response — causing all entity fields to benullregardless of what the subgraph returned.The bug was total:
no-cachebroke entity resolution completely, independently of whether the cache had been populated or not.Additionally, the early return was placed after
cache.fetch_multiple, meaning a Redis round-trip was being made even when the result was going to be discarded.Changes:
no-cacheas all-cache-miss: produce aVec<Option<CacheEntry>>of allNoneentries (one per entity) so thatfilter_representationsbuilds a properly sizedIntermediateResultlist andinsert_entities_in_resultcan assemble the response correctlycache.fetch_multipleinside theelsebranch to avoid the unnecessary Redis round-trip whenno-cacheis setcache_keys.into_iter()tocache_keys.iter()in the span attribute (ownership required sincecache_keysis now used after the span call)Tests added:
no_cache_from_request: two-phase regression test — phase 1 warms the cache with a normal request, phase 2 sendscache-control: no-cacheand asserts entity fields are returned correctly (not null)Test plan
cargo test -p apollo-router --lib plugins::response_cache— 54/54 passed (68 failures are pre-existing Redis integration tests requiring external infra)cargo clippy -p apollo-router --lib -- -D warnings— cleancargo fmt --check— no changesno-cache; fix version always returns correct data regardless of cache state🤖 Generated with Claude Code