Skip to content

Commit a53442d

Browse files
author
Eric Larson
committed
Allow defining a cache as "shared"
In the spec (https://tools.ietf.org/html/rfc7234#section-5.2.2.6) it talks about the `private` directive and that a shared cache MUST NOT store the response. This starts by defining the cache as being "shared" and testing for the "shared" attribute on the cache. Fixes: #141
1 parent c2ac282 commit a53442d

File tree

5 files changed

+63
-4
lines changed

5 files changed

+63
-4
lines changed

cachecontrol/cache.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
class BaseCache(object):
99

10+
def __init__(self, shared=False):
11+
self.shared = shared
12+
1013
def get(self, key):
1114
raise NotImplemented()
1215

@@ -22,7 +25,8 @@ def close(self):
2225

2326
class DictCache(BaseCache):
2427

25-
def __init__(self, init_dict=None):
28+
def __init__(self, init_dict=None, shared=False):
29+
super(DictCache, self).__init__(shared)
2630
self.lock = Lock()
2731
self.data = init_dict or {}
2832

cachecontrol/caches/file_cache.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ def _secure_open_write(filename, fmode):
5454

5555
class FileCache(BaseCache):
5656
def __init__(self, directory, forever=False, filemode=0o0600,
57-
dirmode=0o0700, use_dir_lock=None, lock_class=None):
57+
dirmode=0o0700, use_dir_lock=None, lock_class=None,
58+
shared=False):
59+
60+
super(FileCache, self).__init__(shared)
5861

5962
if use_dir_lock is not None and lock_class is not None:
6063
raise ValueError("Cannot use use_dir_lock and lock_class together")

cachecontrol/caches/redis_cache.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ def total_seconds(td):
1616

1717
class RedisCache(BaseCache):
1818

19-
def __init__(self, conn):
19+
def __init__(self, conn, shared=False):
20+
super(RedisCache, self).__init__(shared)
2021
self.conn = conn
2122

2223
def get(self, key):

cachecontrol/controller.py

+13
Original file line numberDiff line numberDiff line change
@@ -284,10 +284,23 @@ def cache_response(self, request, response, body=None,
284284
if 'no-store' in cc_req:
285285
no_store = True
286286
logger.debug('Request header has "no-store"')
287+
287288
if no_store and self.cache.get(cache_url):
288289
logger.debug('Purging existing cache entry to honor "no-store"')
289290
self.cache.delete(cache_url)
290291

292+
293+
# Check to see if the cache shared or not
294+
is_shared_cache = False
295+
try:
296+
is_shared_cache = self.cache.shared
297+
except AttributeError:
298+
pass
299+
300+
if 'private' in cc and is_shared_cache is True:
301+
logger.debug('Request header has "private" and the cache is shared')
302+
return
303+
291304
# If we've been given an etag, then keep the response
292305
if self.cache_etags and 'etag' in response_headers:
293306
logger.debug('Caching due to etag')

tests/test_cache_control.py

+39-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Unit tests that verify our caching methods work correctly.
33
"""
44
import pytest
5-
from mock import ANY, Mock
5+
from mock import ANY, Mock, patch
66
import time
77

88
from cachecontrol import CacheController
@@ -115,6 +115,44 @@ def test_cache_response_no_store(self):
115115
cc.cache_response(self.req(), resp)
116116
assert not cc.cache.get(cache_url)
117117

118+
def test_cache_response_private_with_shared_cache(self):
119+
'''
120+
When a cache store is shared, a private directive should turn off caching.
121+
122+
In this example, the etag is set, which should trigger a
123+
cache, but since the private directive is set and the cache is
124+
considered shared, we should not cache.
125+
'''
126+
resp = Mock()
127+
cache = DictCache(shared=True)
128+
cc = CacheController(cache)
129+
130+
cache_url = cc.cache_url(self.url)
131+
132+
resp = self.resp({'cache-control': 'max-age=3600, private'})
133+
134+
cc.cache_response(self.req(), resp)
135+
assert not cc.cache.get(cache_url)
136+
137+
def test_cache_response_private_with_legacy_cache(self):
138+
# Not all cache objects will have the "shared" attribute.
139+
resp = Mock()
140+
cache = Mock()
141+
cache.shared.side_effect = AttributeError
142+
cc = CacheController(cache)
143+
cc.serializer = Mock()
144+
145+
cache_url = cc.cache_url(self.url)
146+
147+
now = time.strftime(TIME_FMT, time.gmtime())
148+
resp = self.resp({
149+
'cache-control': 'max-age=3600, private',
150+
'date': now,
151+
})
152+
153+
cc.cache_response(self.req(), resp)
154+
assert cc.cache.set.called
155+
118156
def test_update_cached_response_with_valid_headers(self):
119157
cached_resp = Mock(headers={'ETag': 'jfd9094r808', 'Content-Length': 100})
120158

0 commit comments

Comments
 (0)