diff --git a/django/middleware/cache.py b/django/middleware/cache.py index 3f602fe652..a3076acd22 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -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 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. @@ -71,12 +71,6 @@ class UpdateCacheMiddleware(object): 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- @@ -123,16 +117,25 @@ class FetchFromCacheMiddleware(object): 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 diff --git a/django/utils/cache.py b/django/utils/cache.py index 6cfd893668..b1b35b5a05 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -143,7 +143,7 @@ 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: @@ -151,8 +151,8 @@ def _generate_cache_key(request, headerlist, key_prefix): 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): @@ -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 @@ -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 @@ -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): diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index 5797199411..f2b7a594d3 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -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``: diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py index a91b842ebd..9818b03661 100644 --- a/tests/regressiontests/cache/tests.py +++ b/tests/regressiontests/cache/tests.py @@ -507,12 +507,13 @@ class CacheUtils(unittest.TestCase): 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 @@ -544,18 +545,76 @@ class CacheUtils(unittest.TestCase): 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):