Skip to content

Commit fcef9d3

Browse files
committed
fix(pulpcore): include query string in content-app cache keys
Paginated JSON listings for the same path were sharing one cache entry, so page N was served from page 0 when Redis was enabled. ref #7887 Assisted-By: Cursor
1 parent d3ee9bd commit fcef9d3

3 files changed

Lines changed: 37 additions & 2 deletions

File tree

pulpcore/cache/cache.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class CacheKeys(enum.Enum):
3030
host = "host"
3131
method = "method"
3232
format = "format"
33+
query = "query"
3334

3435

3536
def accept_prefers_json(accept_header):
@@ -373,7 +374,8 @@ def __init__(self, base_key=None, expires_ttl=None, keys=None, auth=None):
373374
keys: a list of CacheKeys to use for key creation upon entry placement,
374375
(path, method) is default. Pass CacheKeys.format if responses for the same
375376
path/method can differ based on the request's Accept header (e.g. JSON vs.
376-
HTML), so that they are cached under separate keys.
377+
HTML). Pass CacheKeys.query if the query string can change the response
378+
(e.g. paginated JSON listings).
377379
auth: a callable to check authorization of the request; takes the request, cache
378380
instance, and base_key as arguments.
379381
"""
@@ -494,13 +496,17 @@ async def make_entry(self, key, base_key, handler, args, kwargs, expires=DEFAULT
494496
def make_key(self, request):
495497
"""Makes the key based off the request"""
496498
# Might potentially have to make this async if keys require async data from request
499+
query_string = getattr(request, "query_string", "")
500+
if not isinstance(query_string, str):
501+
query_string = ""
497502
all_keys = {
498503
CacheKeys.path: request.path,
499504
CacheKeys.method: request.method,
500505
CacheKeys.host: request.url.host,
501506
CacheKeys.format: (
502507
"json" if accept_prefers_json(request.headers.get("Accept")) else "other"
503508
),
509+
CacheKeys.query: query_string,
504510
}
505511
key = ":".join(all_keys[k] for k in self.keys)
506512
return key

pulpcore/content/handler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,9 @@ async def auth_cached(cls, request, cached, base_key):
281281
# header (JSON vs. HTML/binary), so CacheKeys.format must be part of the cache key.
282282
# Without it, a JSON response could be cached and served back for an HTML request
283283
# (or vice versa). See pulpcore.cache.accept_prefers_json.
284-
keys=(CacheKeys.path, CacheKeys.method, CacheKeys.format),
284+
# JSON listings are also paginated via ?limit=&offset=, so CacheKeys.query is
285+
# required or page N would be served from page 0's cache entry.
286+
keys=(CacheKeys.path, CacheKeys.method, CacheKeys.format, CacheKeys.query),
285287
)
286288
async def stream_content(self, request):
287289
"""

pulpcore/tests/unit/test_cache.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,30 @@ def make_request(accept):
160160
assert default_key == html_key
161161
assert star_key == html_key
162162
assert json_key != html_key
163+
164+
165+
def test_content_cache_key_varies_by_query(monkeypatch):
166+
"""Paginated JSON listings for the same path must not share a cache key."""
167+
monkeypatch.setattr("pulpcore.cache.cache.get_async_redis_connection", lambda: None)
168+
cache = AsyncContentCache(
169+
keys=(CacheKeys.path, CacheKeys.method, CacheKeys.format, CacheKeys.query)
170+
)
171+
172+
def make_request(accept, query=""):
173+
request = Mock()
174+
request.path = "/pulp/content/foo/"
175+
request.method = "GET"
176+
request.url.host = "example.com"
177+
request.headers = {"Accept": accept}
178+
request.query_string = query
179+
return request
180+
181+
page0 = cache.make_key(make_request("application/json", "limit=1&offset=0"))
182+
page1 = cache.make_key(make_request("application/json", "limit=1&offset=1"))
183+
no_query = cache.make_key(make_request("application/json", ""))
184+
185+
assert page0 == "/pulp/content/foo/:GET:json:limit=1&offset=0"
186+
assert page1 == "/pulp/content/foo/:GET:json:limit=1&offset=1"
187+
assert no_query == "/pulp/content/foo/:GET:json:"
188+
assert page0 != page1
189+
assert page0 != no_query

0 commit comments

Comments
 (0)