Skip to content
Closed
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
23 changes: 17 additions & 6 deletions cachecontrol/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,12 @@ def _load_from_cache(self, request: PreparedRequest) -> HTTPResponse | None:
else:
body_file = None

result = self.serializer.loads(request, cache_data, body_file)
result = None
try:
result = self.serializer.loads(request, cache_data, body_file)
finally:
if result is None and body_file is not None:
body_file.close()
if result is None:
logger.debug("Cache entry deserialization failed, entry ignored")
return result
Expand Down Expand Up @@ -204,12 +209,14 @@ def cached_request(self, request: PreparedRequest) -> HTTPResponse | Literal[Fal
logger.debug("Purging cached response: no date or etag")
self.cache.delete(cache_url)
logger.debug("Ignoring cached response: no date")
resp.close()
return False

now = time.time()
time_tuple = parsedate_tz(headers["date"])
if time_tuple is None:
logger.debug("Ignoring cached response: invalid date")
resp.close()
return False
date = calendar.timegm(time_tuple[:6])
current_age = max(0, now - date)
Expand Down Expand Up @@ -271,20 +278,24 @@ def cached_request(self, request: PreparedRequest) -> HTTPResponse | Literal[Fal
self.cache.delete(cache_url)

# return the original handler
resp.close()
return False

def conditional_headers(self, request: PreparedRequest) -> dict[str, str]:
resp = self._load_from_cache(request)
new_headers = {}

if resp:
headers: CaseInsensitiveDict[str] = CaseInsensitiveDict(resp.headers)
try:
headers: CaseInsensitiveDict[str] = CaseInsensitiveDict(resp.headers)

if "etag" in headers:
new_headers["If-None-Match"] = headers["ETag"]
if "etag" in headers:
new_headers["If-None-Match"] = headers["ETag"]

if "last-modified" in headers:
new_headers["If-Modified-Since"] = headers["Last-Modified"]
if "last-modified" in headers:
new_headers["If-Modified-Since"] = headers["Last-Modified"]
finally:
resp.close()

return new_headers

Expand Down
23 changes: 23 additions & 0 deletions tests/test_cache_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ def test_no_cache_non_20x_response(self, cc):
assert cc.serializer.dumps.called
assert cc.cache.set.called

def test_load_from_cache_closes_body_on_deserialization_failure(self, tmp_path):
cache = SeparateBodyFileCache(str(tmp_path))
body_file = Mock()
cache.get = Mock(return_value=b"metadata")
cache.get_body = Mock(return_value=body_file)
serializer = Mock()
serializer.loads.return_value = None
controller = CacheController(cache, serializer=serializer)

assert controller._load_from_cache(self.req()) is None
body_file.close.assert_called_once_with()

def test_no_cache_with_no_date(self, cc):
# No date header which makes our max-age pointless
resp = self.resp({"cache-control": "max-age=3600"})
Expand Down Expand Up @@ -367,6 +379,17 @@ def test_cache_request_unfresh_max_age(self):
self.c.cache = DictCache({self.url: resp})
r = self.req({})
assert not r
resp.close.assert_called_once_with()

def test_conditional_headers_closes_cached_response(self):
now = time.strftime(TIME_FMT, time.gmtime())
resp = Mock(headers={"etag": "value", "date": now}, status=200)
self.c.cache = DictCache({self.url: resp})

assert self.c.conditional_headers(Mock(url=self.url, headers={})) == {
"If-None-Match": "value"
}
resp.close.assert_called_once_with()

def test_cache_request_unfresh_permanent_redirect(self):
earlier = time.time() - 3600
Expand Down