Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #14560 -- Enable HEAD requests to be cached properly. Thanks, c…

…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...
commit cb17f7ca2252265ab4a844e7924cb8ebab4a1a76 1 parent 8a72480
Honza Král authored October 29, 2010
21  django/middleware/cache.py
@@ -34,8 +34,8 @@
34 34
   and effective way of avoiding the caching of the Django admin (and any other
35 35
   user-specific content).
36 36
 
37  
-* This middleware expects that a HEAD request is answered with a response
38  
-  exactly like the corresponding GET request.
  37
+* This middleware expects that a HEAD request is answered with the same response
  38
+  headers exactly like the corresponding GET request.
39 39
 
40 40
 * When a hit occurs, a shallow copy of the original response object is returned
41 41
   from process_request.
@@ -71,12 +71,6 @@ def process_response(self, request, response):
71 71
         if not hasattr(request, '_cache_update_cache') or not request._cache_update_cache:
72 72
             # We don't need to update the cache, just return.
73 73
             return response
74  
-        if request.method != 'GET':
75  
-            # This is a stronger requirement than above. It is needed
76  
-            # because of interactions between this middleware and the
77  
-            # HTTPMiddleware, which throws the body of a HEAD-request
78  
-            # away before this middleware gets a chance to cache it.
79  
-            return response
80 74
         if not response.status_code == 200:
81 75
             return response
82 76
         # Try to get the timeout from the "max-age" section of the "Cache-
@@ -123,16 +117,25 @@ def process_request(self, request):
123 117
             request._cache_update_cache = False
124 118
             return None # Don't cache requests from authenticated users.
125 119
 
126  
-        cache_key = get_cache_key(request, self.key_prefix)
  120
+        # try and get the cached GET response
  121
+        cache_key = get_cache_key(request, self.key_prefix, 'GET')
  122
+
127 123
         if cache_key is None:
128 124
             request._cache_update_cache = True
129 125
             return None # No cache information available, need to rebuild.
130 126
 
131 127
         response = cache.get(cache_key, None)
  128
+
  129
+        # if it wasn't found and we are looking for a HEAD, try looking just for that
  130
+        if response is None and request.method == 'HEAD':
  131
+            cache_key = get_cache_key(request, self.key_prefix, 'HEAD')
  132
+            response = cache.get(cache_key, None)
  133
+
132 134
         if response is None:
133 135
             request._cache_update_cache = True
134 136
             return None # No cache information available, need to rebuild.
135 137
 
  138
+        # hit, return cached response
136 139
         request._cache_update_cache = False
137 140
         return response
138 141
 
14  django/utils/cache.py
@@ -143,7 +143,7 @@ def _i18n_cache_key_suffix(request, cache_key):
143 143
         cache_key += '.%s' % getattr(request, 'LANGUAGE_CODE', get_language())
144 144
     return cache_key
145 145
 
146  
-def _generate_cache_key(request, headerlist, key_prefix):
  146
+def _generate_cache_key(request, method, headerlist, key_prefix):
147 147
     """Returns a cache key from the headers given in the header list."""
148 148
     ctx = md5_constructor()
149 149
     for header in headerlist:
@@ -151,8 +151,8 @@ def _generate_cache_key(request, headerlist, key_prefix):
151 151
         if value is not None:
152 152
             ctx.update(value)
153 153
     path = md5_constructor(iri_to_uri(request.path))
154  
-    cache_key = 'views.decorators.cache.cache_page.%s.%s.%s' % (
155  
-        key_prefix, path.hexdigest(), ctx.hexdigest())
  154
+    cache_key = 'views.decorators.cache.cache_page.%s.%s.%s.%s' % (
  155
+        key_prefix, request.method, path.hexdigest(), ctx.hexdigest())
156 156
     return _i18n_cache_key_suffix(request, cache_key)
157 157
 
158 158
 def _generate_cache_header_key(key_prefix, request):
@@ -162,7 +162,7 @@ def _generate_cache_header_key(key_prefix, request):
162 162
         key_prefix, path.hexdigest())
163 163
     return _i18n_cache_key_suffix(request, cache_key)
164 164
 
165  
-def get_cache_key(request, key_prefix=None):
  165
+def get_cache_key(request, key_prefix=None, method='GET'):
166 166
     """
167 167
     Returns a cache key based on the request path. It can be used in the
168 168
     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):
177 177
     cache_key = _generate_cache_header_key(key_prefix, request)
178 178
     headerlist = cache.get(cache_key, None)
179 179
     if headerlist is not None:
180  
-        return _generate_cache_key(request, headerlist, key_prefix)
  180
+        return _generate_cache_key(request, method, headerlist, key_prefix)
181 181
     else:
182 182
         return None
183 183
 
@@ -203,12 +203,12 @@ def learn_cache_key(request, response, cache_timeout=None, key_prefix=None):
203 203
         headerlist = ['HTTP_'+header.upper().replace('-', '_')
204 204
                       for header in cc_delim_re.split(response['Vary'])]
205 205
         cache.set(cache_key, headerlist, cache_timeout)
206  
-        return _generate_cache_key(request, headerlist, key_prefix)
  206
+        return _generate_cache_key(request, request.method, headerlist, key_prefix)
207 207
     else:
208 208
         # if there is no Vary header, we still need a cache key
209 209
         # for the request.path
210 210
         cache.set(cache_key, [], cache_timeout)
211  
-        return _generate_cache_key(request, [], key_prefix)
  211
+        return _generate_cache_key(request, request.method, [], key_prefix)
212 212
 
213 213
 
214 214
 def _to_tuple(s):
4  docs/topics/cache.txt
@@ -328,7 +328,9 @@ parameters. Optionally, if the ``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting is
328 328
 will be cached. This is a simple and effective way of disabling caching for any
329 329
 user-specific pages (include Django's admin interface). Note that if you use
330 330
 ``CACHE_MIDDLEWARE_ANONYMOUS_ONLY``, you should make sure you've activated
331  
-``AuthenticationMiddleware``.
  331
+``AuthenticationMiddleware``. The cache middleware expects that a HEAD request
  332
+is answered with the same response headers exactly like the corresponding GET
  333
+request, in that case it could return cached GET response for HEAD request.
332 334
 
333 335
 Additionally, the cache middleware automatically sets a few headers in each
334 336
 ``HttpResponse``:
69  tests/regressiontests/cache/tests.py
@@ -507,12 +507,13 @@ def tearDown(self):
507 507
         settings.CACHE_MIDDLEWARE_SECONDS = self.old_middleware_seconds
508 508
         settings.USE_I18N = self.orig_use_i18n
509 509
 
510  
-    def _get_request(self, path):
  510
+    def _get_request(self, path, method='GET'):
511 511
         request = HttpRequest()
512 512
         request.META = {
513 513
             'SERVER_NAME': 'testserver',
514 514
             'SERVER_PORT': 80,
515 515
         }
  516
+        request.method = method
516 517
         request.path = request.path_info = "/cache/%s" % path
517 518
         return request
518 519
 
@@ -544,18 +545,76 @@ def test_get_cache_key(self):
544 545
         self.assertEqual(get_cache_key(request), None)
545 546
         # Set headers to an empty list.
546 547
         learn_cache_key(request, response)
547  
-        self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
  548
+        self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
548 549
         # Verify that a specified key_prefix is taken in to account.
549 550
         learn_cache_key(request, response, key_prefix=key_prefix)
550  
-        self.assertEqual(get_cache_key(request, key_prefix=key_prefix), 'views.decorators.cache.cache_page.localprefix.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
  551
+        self.assertEqual(get_cache_key(request, key_prefix=key_prefix), 'views.decorators.cache.cache_page.localprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
551 552
 
552 553
     def test_learn_cache_key(self):
553  
-        request = self._get_request(self.path)
  554
+        request = self._get_request(self.path, 'HEAD')
554 555
         response = HttpResponse()
555 556
         response['Vary'] = 'Pony'
556 557
         # Make sure that the Vary header is added to the key hash
557 558
         learn_cache_key(request, response)
558  
-        self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
  559
+        self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.HEAD.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
  560
+
  561
+class CacheHEADTest(unittest.TestCase):
  562
+
  563
+    def setUp(self):
  564
+        self.orig_cache_middleware_seconds = settings.CACHE_MIDDLEWARE_SECONDS
  565
+        self.orig_cache_middleware_key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
  566
+        self.orig_cache_backend = settings.CACHE_BACKEND
  567
+        settings.CACHE_MIDDLEWARE_SECONDS = 60
  568
+        settings.CACHE_MIDDLEWARE_KEY_PREFIX = 'test'
  569
+        settings.CACHE_BACKEND = 'locmem:///'
  570
+        self.path = '/cache/test/'
  571
+
  572
+    def tearDown(self):
  573
+        settings.CACHE_MIDDLEWARE_SECONDS = self.orig_cache_middleware_seconds
  574
+        settings.CACHE_MIDDLEWARE_KEY_PREFIX = self.orig_cache_middleware_key_prefix
  575
+        settings.CACHE_BACKEND = self.orig_cache_backend
  576
+
  577
+    def _get_request(self, method):
  578
+        request = HttpRequest()
  579
+        request.META = {
  580
+            'SERVER_NAME': 'testserver',
  581
+            'SERVER_PORT': 80,
  582
+        }
  583
+        request.method = method
  584
+        request.path = request.path_info = self.path
  585
+        return request
  586
+
  587
+    def _get_request_cache(self, method):
  588
+        request = self._get_request(method)
  589
+        request._cache_update_cache = True
  590
+        return request
  591
+
  592
+    def _set_cache(self, request, msg):
  593
+        response = HttpResponse()
  594
+        response.content = msg
  595
+        return UpdateCacheMiddleware().process_response(request, response)
  596
+
  597
+    def test_head_caches_correctly(self):
  598
+        test_content = 'test content'
  599
+
  600
+        request = self._get_request_cache('HEAD')
  601
+        self._set_cache(request, test_content)
  602
+
  603
+        request = self._get_request('HEAD')
  604
+        get_cache_data = FetchFromCacheMiddleware().process_request(request)
  605
+        self.assertNotEqual(get_cache_data, None)
  606
+        self.assertEqual(test_content, get_cache_data.content)
  607
+
  608
+    def test_head_with_cached_get(self):
  609
+        test_content = 'test content'
  610
+
  611
+        request = self._get_request_cache('GET')
  612
+        self._set_cache(request, test_content)
  613
+
  614
+        request = self._get_request('HEAD')
  615
+        get_cache_data = FetchFromCacheMiddleware().process_request(request)
  616
+        self.assertNotEqual(get_cache_data, None)
  617
+        self.assertEqual(test_content, get_cache_data.content)
559 618
 
560 619
 class CacheI18nTest(unittest.TestCase):
561 620
 

0 notes on commit cb17f7c

Please sign in to comment.
Something went wrong with that request. Please try again.