Skip to content

Commit 267b8f9

Browse files
committed
Make Serializer.dumps() require a body parameter.
When caching permanent redirects, if `body` is left to `None`, there's an infinite recursion that will lead to the caching to silently fail and not cache anything at all. So instead, make `body` a required parameter, which can be empty (`''`) for cached redirects.
1 parent db0dcc2 commit 267b8f9

File tree

4 files changed

+17
-27
lines changed

4 files changed

+17
-27
lines changed

cachecontrol/controller.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import re
66
import calendar
77
import time
8+
import traceback
89
from email.utils import parsedate_tz
910

1011
from requests.structures import CaseInsensitiveDict
@@ -280,7 +281,7 @@ def cache_response(self, request, response, body=None, status_codes=None):
280281
cc = self.parse_cache_control(response_headers)
281282

282283
cache_url = self.cache_url(request.url)
283-
logger.debug('Updating cache %r with response from "%s"', self.cache, cache_url)
284+
logger.debug('Updating cache with response from "%s"', cache_url)
284285

285286
# Delete it from the cache if we happen to have it stored there
286287
no_store = False
@@ -309,14 +310,14 @@ def cache_response(self, request, response, body=None, status_codes=None):
309310
if self.cache_etags and "etag" in response_headers:
310311
logger.debug("Caching due to etag")
311312
self.cache.set(
312-
cache_url, self.serializer.dumps(request, response, body=body)
313+
cache_url, self.serializer.dumps(request, response, body)
313314
)
314315

315316
# Add to the cache any permanent redirects. We do this before looking
316317
# that the Date headers.
317318
elif int(response.status) in PERMANENT_REDIRECT_STATUSES:
318319
logger.debug("Caching permanent redirect")
319-
self.cache.set(cache_url, self.serializer.dumps(request, response))
320+
self.cache.set(cache_url, self.serializer.dumps(request, response, b''))
320321

321322
# Add to the cache if the response headers demand it. If there
322323
# is no date header then we can't do anything about expiring
@@ -329,7 +330,7 @@ def cache_response(self, request, response, body=None, status_codes=None):
329330
if "max-age" in cc and cc["max-age"] > 0:
330331
logger.debug("Caching b/c date exists and max-age > 0")
331332
self.cache.set(
332-
cache_url, self.serializer.dumps(request, response, body=body)
333+
cache_url, self.serializer.dumps(request, response, body)
333334
)
334335

335336
# If the request can expire, it means we should cache it
@@ -338,7 +339,7 @@ def cache_response(self, request, response, body=None, status_codes=None):
338339
if response_headers["expires"]:
339340
logger.debug("Caching b/c of expires header")
340341
self.cache.set(
341-
cache_url, self.serializer.dumps(request, response, body=body)
342+
cache_url, self.serializer.dumps(request, response, body)
342343
)
343344
else:
344345
logger.debug("No combination of headers to cache.")
@@ -379,6 +380,7 @@ def update_cached_response(self, request, response):
379380
cached_response.status = 200
380381

381382
# update our cache
382-
self.cache.set(cache_url, self.serializer.dumps(request, cached_response))
383+
body = cached_response.read(decode_content=False)
384+
self.cache.set(cache_url, self.serializer.dumps(request, cached_response, body))
383385

384386
return cached_response

cachecontrol/serialize.py

+4-16
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,14 @@ def _b64_decode_str(s):
1717
return _b64_decode_bytes(s).decode("utf8")
1818

1919

20+
_default_body_read = object()
21+
22+
2023
class Serializer(object):
2124

22-
def dumps(self, request, response, body=None):
25+
def dumps(self, request, response, body):
2326
response_headers = CaseInsensitiveDict(response.headers)
2427

25-
if body is None:
26-
body = response.read(decode_content=False)
27-
28-
# NOTE: 99% sure this is dead code. I'm only leaving it
29-
# here b/c I don't have a test yet to prove
30-
# it. Basically, before using
31-
# `cachecontrol.filewrapper.CallbackFileWrapper`,
32-
# this made an effort to reset the file handle. The
33-
# `CallbackFileWrapper` short circuits this code by
34-
# setting the body as the content is consumed, the
35-
# result being a `body` argument is *always* passed
36-
# into cache_response, and in turn,
37-
# `Serializer.dump`.
38-
response._fp = io.BytesIO(body)
39-
4028
# NOTE: This is all a bit weird, but it's really important that on
4129
# Python 2.x these objects are unicode and not str, even when
4230
# they contain only ascii. The problem here is that msgpack

tests/test_cache_control.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def test_no_cache_with_wrong_sized_body(self, cc):
6767
# When the body is the wrong size, then we don't want to cache it
6868
# because it is obviously broken.
6969
resp = self.resp({"cache-control": "max-age=3600", "Content-Length": "5"})
70-
cc.cache_response(self.req(), resp, body=b"0" * 10)
70+
cc.cache_response(self.req(), resp, b"0" * 10)
7171

7272
assert not cc.cache.set.called
7373

@@ -82,7 +82,7 @@ def test_cache_response_cache_max_age(self, cc):
8282
resp = self.resp({"cache-control": "max-age=3600", "date": now})
8383
req = self.req()
8484
cc.cache_response(req, resp)
85-
cc.serializer.dumps.assert_called_with(req, resp, body=None)
85+
cc.serializer.dumps.assert_called_with(req, resp, None)
8686
cc.cache.set.assert_called_with(self.url, ANY)
8787

8888
def test_cache_response_cache_max_age_with_invalid_value_not_cached(self, cc):

tests/test_serialization.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def test_read_latest_version_streamable(self, url):
8989
original_resp = requests.get(url, stream=True)
9090
req = original_resp.request
9191

92-
resp = self.serializer.loads(req, self.serializer.dumps(req, original_resp.raw))
92+
resp = self.serializer.loads(req, self.serializer.dumps(req, original_resp.raw, original_resp.content))
9393

9494
assert resp.read()
9595

@@ -99,7 +99,7 @@ def test_read_latest_version(self, url):
9999
req = original_resp.request
100100

101101
resp = self.serializer.loads(
102-
req, self.serializer.dumps(req, original_resp.raw, body=data)
102+
req, self.serializer.dumps(req, original_resp.raw, data)
103103
)
104104

105105
assert resp.read() == data
@@ -114,5 +114,5 @@ def test_no_vary_header(self, url):
114114
original_resp.raw.headers["vary"] = "Foo"
115115

116116
assert self.serializer.loads(
117-
req, self.serializer.dumps(req, original_resp.raw, body=data)
117+
req, self.serializer.dumps(req, original_resp.raw, data)
118118
)

0 commit comments

Comments
 (0)