Fixed #14560 -- Enable HEAD requests to be cached properly. Thanks, codemonkey!
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
This commit is contained in:
parent
8a724802c5
commit
cb17f7ca22
|
@ -34,8 +34,8 @@ More details about how the caching works:
|
||||||
and effective way of avoiding the caching of the Django admin (and any other
|
and effective way of avoiding the caching of the Django admin (and any other
|
||||||
user-specific content).
|
user-specific content).
|
||||||
|
|
||||||
* This middleware expects that a HEAD request is answered with a response
|
* This middleware expects that a HEAD request is answered with the same response
|
||||||
exactly like the corresponding GET request.
|
headers exactly like the corresponding GET request.
|
||||||
|
|
||||||
* When a hit occurs, a shallow copy of the original response object is returned
|
* When a hit occurs, a shallow copy of the original response object is returned
|
||||||
from process_request.
|
from process_request.
|
||||||
|
@ -71,12 +71,6 @@ class UpdateCacheMiddleware(object):
|
||||||
if not hasattr(request, '_cache_update_cache') or not request._cache_update_cache:
|
if not hasattr(request, '_cache_update_cache') or not request._cache_update_cache:
|
||||||
# We don't need to update the cache, just return.
|
# We don't need to update the cache, just return.
|
||||||
return response
|
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:
|
if not response.status_code == 200:
|
||||||
return response
|
return response
|
||||||
# Try to get the timeout from the "max-age" section of the "Cache-
|
# Try to get the timeout from the "max-age" section of the "Cache-
|
||||||
|
@ -123,16 +117,25 @@ class FetchFromCacheMiddleware(object):
|
||||||
request._cache_update_cache = False
|
request._cache_update_cache = False
|
||||||
return None # Don't cache requests from authenticated users.
|
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:
|
if cache_key is None:
|
||||||
request._cache_update_cache = True
|
request._cache_update_cache = True
|
||||||
return None # No cache information available, need to rebuild.
|
return None # No cache information available, need to rebuild.
|
||||||
|
|
||||||
response = cache.get(cache_key, None)
|
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:
|
if response is None:
|
||||||
request._cache_update_cache = True
|
request._cache_update_cache = True
|
||||||
return None # No cache information available, need to rebuild.
|
return None # No cache information available, need to rebuild.
|
||||||
|
|
||||||
|
# hit, return cached response
|
||||||
request._cache_update_cache = False
|
request._cache_update_cache = False
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
|
@ -143,7 +143,7 @@ def _i18n_cache_key_suffix(request, cache_key):
|
||||||
cache_key += '.%s' % getattr(request, 'LANGUAGE_CODE', get_language())
|
cache_key += '.%s' % getattr(request, 'LANGUAGE_CODE', get_language())
|
||||||
return cache_key
|
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."""
|
"""Returns a cache key from the headers given in the header list."""
|
||||||
ctx = md5_constructor()
|
ctx = md5_constructor()
|
||||||
for header in headerlist:
|
for header in headerlist:
|
||||||
|
@ -151,8 +151,8 @@ def _generate_cache_key(request, headerlist, key_prefix):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
ctx.update(value)
|
ctx.update(value)
|
||||||
path = md5_constructor(iri_to_uri(request.path))
|
path = md5_constructor(iri_to_uri(request.path))
|
||||||
cache_key = 'views.decorators.cache.cache_page.%s.%s.%s' % (
|
cache_key = 'views.decorators.cache.cache_page.%s.%s.%s.%s' % (
|
||||||
key_prefix, path.hexdigest(), ctx.hexdigest())
|
key_prefix, request.method, path.hexdigest(), ctx.hexdigest())
|
||||||
return _i18n_cache_key_suffix(request, cache_key)
|
return _i18n_cache_key_suffix(request, cache_key)
|
||||||
|
|
||||||
def _generate_cache_header_key(key_prefix, request):
|
def _generate_cache_header_key(key_prefix, request):
|
||||||
|
@ -162,7 +162,7 @@ def _generate_cache_header_key(key_prefix, request):
|
||||||
key_prefix, path.hexdigest())
|
key_prefix, path.hexdigest())
|
||||||
return _i18n_cache_key_suffix(request, cache_key)
|
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
|
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
|
request phase because it pulls the list of headers to take into account
|
||||||
|
@ -177,7 +177,7 @@ def get_cache_key(request, key_prefix=None):
|
||||||
cache_key = _generate_cache_header_key(key_prefix, request)
|
cache_key = _generate_cache_header_key(key_prefix, request)
|
||||||
headerlist = cache.get(cache_key, None)
|
headerlist = cache.get(cache_key, None)
|
||||||
if headerlist is not None:
|
if headerlist is not None:
|
||||||
return _generate_cache_key(request, headerlist, key_prefix)
|
return _generate_cache_key(request, method, headerlist, key_prefix)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -203,12 +203,12 @@ def learn_cache_key(request, response, cache_timeout=None, key_prefix=None):
|
||||||
headerlist = ['HTTP_'+header.upper().replace('-', '_')
|
headerlist = ['HTTP_'+header.upper().replace('-', '_')
|
||||||
for header in cc_delim_re.split(response['Vary'])]
|
for header in cc_delim_re.split(response['Vary'])]
|
||||||
cache.set(cache_key, headerlist, cache_timeout)
|
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:
|
else:
|
||||||
# if there is no Vary header, we still need a cache key
|
# if there is no Vary header, we still need a cache key
|
||||||
# for the request.path
|
# for the request.path
|
||||||
cache.set(cache_key, [], cache_timeout)
|
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):
|
def _to_tuple(s):
|
||||||
|
|
|
@ -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
|
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
|
user-specific pages (include Django's admin interface). Note that if you use
|
||||||
``CACHE_MIDDLEWARE_ANONYMOUS_ONLY``, you should make sure you've activated
|
``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
|
Additionally, the cache middleware automatically sets a few headers in each
|
||||||
``HttpResponse``:
|
``HttpResponse``:
|
||||||
|
|
|
@ -507,12 +507,13 @@ class CacheUtils(unittest.TestCase):
|
||||||
settings.CACHE_MIDDLEWARE_SECONDS = self.old_middleware_seconds
|
settings.CACHE_MIDDLEWARE_SECONDS = self.old_middleware_seconds
|
||||||
settings.USE_I18N = self.orig_use_i18n
|
settings.USE_I18N = self.orig_use_i18n
|
||||||
|
|
||||||
def _get_request(self, path):
|
def _get_request(self, path, method='GET'):
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
request.META = {
|
request.META = {
|
||||||
'SERVER_NAME': 'testserver',
|
'SERVER_NAME': 'testserver',
|
||||||
'SERVER_PORT': 80,
|
'SERVER_PORT': 80,
|
||||||
}
|
}
|
||||||
|
request.method = method
|
||||||
request.path = request.path_info = "/cache/%s" % path
|
request.path = request.path_info = "/cache/%s" % path
|
||||||
return request
|
return request
|
||||||
|
|
||||||
|
@ -544,18 +545,76 @@ class CacheUtils(unittest.TestCase):
|
||||||
self.assertEqual(get_cache_key(request), None)
|
self.assertEqual(get_cache_key(request), None)
|
||||||
# Set headers to an empty list.
|
# Set headers to an empty list.
|
||||||
learn_cache_key(request, response)
|
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.
|
# Verify that a specified key_prefix is taken in to account.
|
||||||
learn_cache_key(request, response, key_prefix=key_prefix)
|
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):
|
def test_learn_cache_key(self):
|
||||||
request = self._get_request(self.path)
|
request = self._get_request(self.path, 'HEAD')
|
||||||
response = HttpResponse()
|
response = HttpResponse()
|
||||||
response['Vary'] = 'Pony'
|
response['Vary'] = 'Pony'
|
||||||
# Make sure that the Vary header is added to the key hash
|
# Make sure that the Vary header is added to the key hash
|
||||||
learn_cache_key(request, response)
|
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):
|
class CacheI18nTest(unittest.TestCase):
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue