Skip to content

Commit

Permalink
Fixed #14560 -- Enable HEAD requests to be cached properly. Thanks, c…
Browse files Browse the repository at this point in the history
…odemonkey!

Introducing ability to cache HEAD requests and GET requests separately by
adding the method to the cache key while preserving the functionality that HEAD
requests can use cached reponses generated by a GET request.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14391 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
honzakral committed Oct 29, 2010
1 parent 8a72480 commit cb17f7c
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 22 deletions.
21 changes: 12 additions & 9 deletions django/middleware/cache.py
Expand Up @@ -34,8 +34,8 @@
and effective way of avoiding the caching of the Django admin (and any other
user-specific content).
* This middleware expects that a HEAD request is answered with a response
exactly like the corresponding GET request.
* This middleware expects that a HEAD request is answered with the same response
headers exactly like the corresponding GET request.
* When a hit occurs, a shallow copy of the original response object is returned
from process_request.
Expand Down Expand Up @@ -71,12 +71,6 @@ def process_response(self, request, response):
if not hasattr(request, '_cache_update_cache') or not request._cache_update_cache:
# We don't need to update the cache, just return.
return response
if request.method != 'GET':
# This is a stronger requirement than above. It is needed
# because of interactions between this middleware and the
# HTTPMiddleware, which throws the body of a HEAD-request
# away before this middleware gets a chance to cache it.
return response
if not response.status_code == 200:
return response
# Try to get the timeout from the "max-age" section of the "Cache-
Expand Down Expand Up @@ -123,16 +117,25 @@ def process_request(self, request):
request._cache_update_cache = False
return None # Don't cache requests from authenticated users.

cache_key = get_cache_key(request, self.key_prefix)
# try and get the cached GET response
cache_key = get_cache_key(request, self.key_prefix, 'GET')

if cache_key is None:
request._cache_update_cache = True
return None # No cache information available, need to rebuild.

response = cache.get(cache_key, None)

# if it wasn't found and we are looking for a HEAD, try looking just for that
if response is None and request.method == 'HEAD':
cache_key = get_cache_key(request, self.key_prefix, 'HEAD')
response = cache.get(cache_key, None)

if response is None:
request._cache_update_cache = True
return None # No cache information available, need to rebuild.

# hit, return cached response
request._cache_update_cache = False
return response

Expand Down
14 changes: 7 additions & 7 deletions django/utils/cache.py
Expand Up @@ -143,16 +143,16 @@ def _i18n_cache_key_suffix(request, cache_key):
cache_key += '.%s' % getattr(request, 'LANGUAGE_CODE', get_language())
return cache_key

def _generate_cache_key(request, headerlist, key_prefix):
def _generate_cache_key(request, method, headerlist, key_prefix):
"""Returns a cache key from the headers given in the header list."""
ctx = md5_constructor()
for header in headerlist:
value = request.META.get(header, None)
if value is not None:
ctx.update(value)
path = md5_constructor(iri_to_uri(request.path))
cache_key = 'views.decorators.cache.cache_page.%s.%s.%s' % (
key_prefix, path.hexdigest(), ctx.hexdigest())
cache_key = 'views.decorators.cache.cache_page.%s.%s.%s.%s' % (
key_prefix, request.method, path.hexdigest(), ctx.hexdigest())
return _i18n_cache_key_suffix(request, cache_key)

def _generate_cache_header_key(key_prefix, request):
Expand All @@ -162,7 +162,7 @@ def _generate_cache_header_key(key_prefix, request):
key_prefix, path.hexdigest())
return _i18n_cache_key_suffix(request, cache_key)

def get_cache_key(request, key_prefix=None):
def get_cache_key(request, key_prefix=None, method='GET'):
"""
Returns a cache key based on the request path. It can be used in the
request phase because it pulls the list of headers to take into account
Expand All @@ -177,7 +177,7 @@ def get_cache_key(request, key_prefix=None):
cache_key = _generate_cache_header_key(key_prefix, request)
headerlist = cache.get(cache_key, None)
if headerlist is not None:
return _generate_cache_key(request, headerlist, key_prefix)
return _generate_cache_key(request, method, headerlist, key_prefix)
else:
return None

Expand All @@ -203,12 +203,12 @@ def learn_cache_key(request, response, cache_timeout=None, key_prefix=None):
headerlist = ['HTTP_'+header.upper().replace('-', '_')
for header in cc_delim_re.split(response['Vary'])]
cache.set(cache_key, headerlist, cache_timeout)
return _generate_cache_key(request, headerlist, key_prefix)
return _generate_cache_key(request, request.method, headerlist, key_prefix)
else:
# if there is no Vary header, we still need a cache key
# for the request.path
cache.set(cache_key, [], cache_timeout)
return _generate_cache_key(request, [], key_prefix)
return _generate_cache_key(request, request.method, [], key_prefix)


def _to_tuple(s):
Expand Down
4 changes: 3 additions & 1 deletion docs/topics/cache.txt
Expand Up @@ -328,7 +328,9 @@ parameters. Optionally, if the ``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting is
will be cached. This is a simple and effective way of disabling caching for any
user-specific pages (include Django's admin interface). Note that if you use
``CACHE_MIDDLEWARE_ANONYMOUS_ONLY``, you should make sure you've activated
``AuthenticationMiddleware``.
``AuthenticationMiddleware``. The cache middleware expects that a HEAD request
is answered with the same response headers exactly like the corresponding GET
request, in that case it could return cached GET response for HEAD request.

Additionally, the cache middleware automatically sets a few headers in each
``HttpResponse``:
Expand Down
69 changes: 64 additions & 5 deletions tests/regressiontests/cache/tests.py
Expand Up @@ -507,12 +507,13 @@ def tearDown(self):
settings.CACHE_MIDDLEWARE_SECONDS = self.old_middleware_seconds
settings.USE_I18N = self.orig_use_i18n

def _get_request(self, path):
def _get_request(self, path, method='GET'):
request = HttpRequest()
request.META = {
'SERVER_NAME': 'testserver',
'SERVER_PORT': 80,
}
request.method = method
request.path = request.path_info = "/cache/%s" % path
return request

Expand Down Expand Up @@ -544,18 +545,76 @@ def test_get_cache_key(self):
self.assertEqual(get_cache_key(request), None)
# Set headers to an empty list.
learn_cache_key(request, response)
self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
# Verify that a specified key_prefix is taken in to account.
learn_cache_key(request, response, key_prefix=key_prefix)
self.assertEqual(get_cache_key(request, key_prefix=key_prefix), 'views.decorators.cache.cache_page.localprefix.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
self.assertEqual(get_cache_key(request, key_prefix=key_prefix), 'views.decorators.cache.cache_page.localprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')

def test_learn_cache_key(self):
request = self._get_request(self.path)
request = self._get_request(self.path, 'HEAD')
response = HttpResponse()
response['Vary'] = 'Pony'
# Make sure that the Vary header is added to the key hash
learn_cache_key(request, response)
self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.HEAD.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')

class CacheHEADTest(unittest.TestCase):

def setUp(self):
self.orig_cache_middleware_seconds = settings.CACHE_MIDDLEWARE_SECONDS
self.orig_cache_middleware_key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
self.orig_cache_backend = settings.CACHE_BACKEND
settings.CACHE_MIDDLEWARE_SECONDS = 60
settings.CACHE_MIDDLEWARE_KEY_PREFIX = 'test'
settings.CACHE_BACKEND = 'locmem:///'
self.path = '/cache/test/'

def tearDown(self):
settings.CACHE_MIDDLEWARE_SECONDS = self.orig_cache_middleware_seconds
settings.CACHE_MIDDLEWARE_KEY_PREFIX = self.orig_cache_middleware_key_prefix
settings.CACHE_BACKEND = self.orig_cache_backend

def _get_request(self, method):
request = HttpRequest()
request.META = {
'SERVER_NAME': 'testserver',
'SERVER_PORT': 80,
}
request.method = method
request.path = request.path_info = self.path
return request

def _get_request_cache(self, method):
request = self._get_request(method)
request._cache_update_cache = True
return request

def _set_cache(self, request, msg):
response = HttpResponse()
response.content = msg
return UpdateCacheMiddleware().process_response(request, response)

def test_head_caches_correctly(self):
test_content = 'test content'

request = self._get_request_cache('HEAD')
self._set_cache(request, test_content)

request = self._get_request('HEAD')
get_cache_data = FetchFromCacheMiddleware().process_request(request)
self.assertNotEqual(get_cache_data, None)
self.assertEqual(test_content, get_cache_data.content)

def test_head_with_cached_get(self):
test_content = 'test content'

request = self._get_request_cache('GET')
self._set_cache(request, test_content)

request = self._get_request('HEAD')
get_cache_data = FetchFromCacheMiddleware().process_request(request)
self.assertNotEqual(get_cache_data, None)
self.assertEqual(test_content, get_cache_data.content)

class CacheI18nTest(unittest.TestCase):

Expand Down

0 comments on commit cb17f7c

Please sign in to comment.