Robert Brewer committed 54a51a4

Forward port to trunk from 3.0.x [1703]. Added checking of 'Vary' header before responding with cached content.

Comments (0)

Files changed (3)


     request.cacheable = not c
     if c:
         response = cherrypy.response
-        s, h, b, create_time = cache_data
+        s, h, b, create_time, original_req_headers = cache_data
-        # Make a copy. See
+        # Check 'Vary' selecting headers. If any headers mentioned in "Vary"
+        # differ between the cached and current request, bail out and
+        # let the rest of CP handle the request. This should properly
+        # mimic the behavior of isolated caches as RFC 2616 assumes:
+        # "If the selecting request header fields for the cached entry
+        # do not match the selecting request header fields of the new
+        # request, then the cache MUST NOT use a cached entry to satisfy
+        # the request unless it first relays the new request to the origin
+        # server in a conditional request and the server responds with
+        # 304 (Not Modified), including an entity tag or Content-Location
+        # that indicates the entity to be used.
+        # TODO: can we store multiple variants based on Vary'd headers?
+        for header_element in h.elements('Vary'):
+            key = header_element.value
+            if original_req_headers[key] != request.headers.get(key, 'missing'):
+                request.cached = False
+                request.cacheable = True
+                return False
+        # Copy the response headers. See
         response.headers = rh = http.HeaderMap()
         for k in h:
             dict.__setitem__(rh, k, dict.__getitem__(h, k))
         if response.headers.get('Pragma', None) != 'no-cache':
             # save the cache data
             body = ''.join(output)
+            vary = [he.value for he in
+                    cherrypy.response.headers.elements('Vary')]
+            if vary:
+                sel_headers = dict([(k, v) for k, v
+                                    in cherrypy.request.headers.iteritems()
+                                    if k in vary])
+            else:
+                sel_headers = {}
             cherrypy._cache.put((response.status, response.headers or {},
-                                 body, response.time))
+                                 body, response.time, sel_headers))
     response = cherrypy.response
     response.body = tee(response.body)


 from cherrypy.test import test
+import gzip
 import os
 curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+import StringIO
 import cherrypy
 from cherrypy.lib import http
     cherrypy.tree.mount(UnCached(), "/expires")
-    cherrypy.config.update({'environment': 'test_suite'})
+    cherrypy.config.update({'environment': 'test_suite',
+                            'tools.gzip.on': True})
 from cherrypy.test import helper
         self.assertBody('visit #3')
         self.getPage("/", method="DELETE")
         self.assertBody('visit #4')
         # The previous request should have invalidated the cache,
         # so this request will recalc the response.
+        zbuf = StringIO.StringIO()
+        zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
+        zfile.write("visit #5")
+        zfile.close()
+        self.getPage("/", method="GET", headers=[('Accept-Encoding', 'gzip')])
+        self.assertHeader('Content-Encoding', 'gzip')
+        self.assertInBody(zbuf.getvalue()[:3])
+        # Now check that a second request gets the gzip header and gzipped body
+        self.getPage("/", method="GET", headers=[('Accept-Encoding', 'gzip')])
+        self.assertHeader('Content-Encoding', 'gzip')
+        self.assertInBody(zbuf.getvalue()[:3])
+        # Now check that a third request that doesn't accept gzip
+        # gets another hit.
         self.getPage("/", method="GET")
-        self.assertBody('visit #5')
+        self.assertNoHeader('Content-Encoding')
+        self.assertBody('visit #6')
     def testExpiresTool(self):


         self.getPage('/gzip/', headers=[("Accept-Encoding", "gzip")])
         self.assertHeader("Vary", "Accept-Encoding")
+        self.assertHeader("Content-Encoding", "gzip")
         # Test when gzip is denied.
         self.getPage('/gzip/', headers=[("Accept-Encoding", "identity")])
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.