Skip to content

Commit

Permalink
Allow defining a cache as "shared"
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Eric Larson committed Aug 7, 2017
1 parent c2ac282 commit a53442d
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 4 deletions.
6 changes: 5 additions & 1 deletion cachecontrol/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

class BaseCache(object):

def __init__(self, shared=False):
self.shared = shared

def get(self, key):
raise NotImplemented()

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

class DictCache(BaseCache):

def __init__(self, init_dict=None):
def __init__(self, init_dict=None, shared=False):
super(DictCache, self).__init__(shared)
self.lock = Lock()
self.data = init_dict or {}

Expand Down
5 changes: 4 additions & 1 deletion cachecontrol/caches/file_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ def _secure_open_write(filename, fmode):

class FileCache(BaseCache):
def __init__(self, directory, forever=False, filemode=0o0600,
dirmode=0o0700, use_dir_lock=None, lock_class=None):
dirmode=0o0700, use_dir_lock=None, lock_class=None,
shared=False):

super(FileCache, self).__init__(shared)

if use_dir_lock is not None and lock_class is not None:
raise ValueError("Cannot use use_dir_lock and lock_class together")
Expand Down
3 changes: 2 additions & 1 deletion cachecontrol/caches/redis_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ def total_seconds(td):

class RedisCache(BaseCache):

def __init__(self, conn):
def __init__(self, conn, shared=False):
super(RedisCache, self).__init__(shared)
self.conn = conn

def get(self, key):
Expand Down
13 changes: 13 additions & 0 deletions cachecontrol/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,23 @@ def cache_response(self, request, response, body=None,
if 'no-store' in cc_req:
no_store = True
logger.debug('Request header has "no-store"')

if no_store and self.cache.get(cache_url):
logger.debug('Purging existing cache entry to honor "no-store"')
self.cache.delete(cache_url)


# Check to see if the cache shared or not
is_shared_cache = False
try:
is_shared_cache = self.cache.shared
except AttributeError:
pass

if 'private' in cc and is_shared_cache is True:
logger.debug('Request header has "private" and the cache is shared')
return

# If we've been given an etag, then keep the response
if self.cache_etags and 'etag' in response_headers:
logger.debug('Caching due to etag')
Expand Down
40 changes: 39 additions & 1 deletion tests/test_cache_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Unit tests that verify our caching methods work correctly.
"""
import pytest
from mock import ANY, Mock
from mock import ANY, Mock, patch
import time

from cachecontrol import CacheController
Expand Down Expand Up @@ -115,6 +115,44 @@ def test_cache_response_no_store(self):
cc.cache_response(self.req(), resp)
assert not cc.cache.get(cache_url)

def test_cache_response_private_with_shared_cache(self):
'''
When a cache store is shared, a private directive should turn off caching.
In this example, the etag is set, which should trigger a
cache, but since the private directive is set and the cache is
considered shared, we should not cache.
'''
resp = Mock()
cache = DictCache(shared=True)
cc = CacheController(cache)

cache_url = cc.cache_url(self.url)

resp = self.resp({'cache-control': 'max-age=3600, private'})

cc.cache_response(self.req(), resp)
assert not cc.cache.get(cache_url)

def test_cache_response_private_with_legacy_cache(self):
# Not all cache objects will have the "shared" attribute.
resp = Mock()
cache = Mock()
cache.shared.side_effect = AttributeError
cc = CacheController(cache)
cc.serializer = Mock()

cache_url = cc.cache_url(self.url)

now = time.strftime(TIME_FMT, time.gmtime())
resp = self.resp({
'cache-control': 'max-age=3600, private',
'date': now,
})

cc.cache_response(self.req(), resp)
assert cc.cache.set.called

def test_update_cached_response_with_valid_headers(self):
cached_resp = Mock(headers={'ETag': 'jfd9094r808', 'Content-Length': 100})

Expand Down

0 comments on commit a53442d

Please sign in to comment.