Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 11 additions & 17 deletions cachecontrol/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,23 +190,11 @@ def cached_request(self, request: PreparedRequest) -> HTTPResponse | Literal[Fal
if not resp:
return False

# If we have a cached permanent redirect, return it immediately. We
# don't need to test our response for other headers b/c it is
# intrinsically "cacheable" as it is Permanent.
#
# See:
# https://tools.ietf.org/html/rfc7231#section-6.4.2
#
# Client can try to refresh the value by repeating the request
# with cache busting headers as usual (ie no-cache).
if int(resp.status) in PERMANENT_REDIRECT_STATUSES:
msg = (
"Returning cached permanent redirect response "
"(ignoring date and etag information)"
)
logger.debug(msg)
return resp

# Apply normal freshness checks to every cached response. In the case
# of permanent redirects, RFC 9110 sections 15.4.2 and 15.4.9 permit
# heuristic caching, but RFC 9111 section 4.2.2 requires explicit
# expiration to take precedence. The redirect fallback below is used
# only when neither max-age nor Expires provides an expiration.
headers: CaseInsensitiveDict[str] = CaseInsensitiveDict(resp.headers)
if not headers or "date" not in headers:
if "etag" not in headers:
Expand Down Expand Up @@ -249,6 +237,12 @@ def cached_request(self, request: PreparedRequest) -> HTTPResponse | Literal[Fal
freshness_lifetime = max(0, expire_time)
logger.debug("Freshness lifetime from expires: %i", freshness_lifetime)

# Permanent redirects are heuristically cacheable when the response
# does not provide an explicit freshness lifetime.
elif int(resp.status) in PERMANENT_REDIRECT_STATUSES:
logger.debug("Returning cached permanent redirect response")
return resp

# Determine if we are setting freshness limit in the
# request. Note, this overrides what was in the response.
max_age = cc.get("max-age")
Expand Down
8 changes: 8 additions & 0 deletions tests/test_cache_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,14 @@ def test_cache_request_unfresh_max_age(self):
r = self.req({})
assert not r

def test_cache_request_unfresh_permanent_redirect(self):
earlier = time.time() - 3600
date = time.strftime(TIME_FMT, time.gmtime(earlier))
resp = Mock(headers={"cache-control": "max-age=1", "date": date}, status=301)
self.c.cache = DictCache({self.url: resp})
r = self.req({})
assert not r

def test_cache_request_fresh_expires(self):
later = time.time() + 86400 # GMT + 1 day
expires = time.strftime(TIME_FMT, time.gmtime(later))
Expand Down