Commits

Robert Brewer committed db0384d

Fix for #918 (caching does not respect Cache-Control: max-age header).

  • Participants
  • Parent commits 726e0b5

Comments (0)

Files changed (3)

cherrypy/lib/caching.py

                 uricache[tuple(header_values)] = variant
                 self.tot_puts += 1
                 self.cursize = total_size
-                return
     
     def delete(self):
         """Remove ALL cached variants of the current resource."""
         request.cacheable = False
         return False
     
+    if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]:
+        request.cached = False
+        request.cacheable = True
+        return False
+    
     cache_data = cherrypy._cache.get()
     request.cached = bool(cache_data)
     request.cacheable = not request.cached
     if request.cached:
         # Serve the cached copy.
+        max_age = cherrypy._cache.delay
+        for v in [e.value for e in request.headers.elements('Cache-Control')]:
+            atoms = v.split('=', 1)
+            directive = atoms.pop(0)
+            if directive == 'max-age':
+                if len(atoms) != 1 or not atoms[0].isdigit():
+                    raise cherrypy.HTTPError(400, "Invalid Cache-Control header")
+                max_age = int(atoms[0])
+                break
+            elif directive == 'no-cache':
+                if debug:
+                    cherrypy.log('Ignoring cache due to Cache-Control: no-cache',
+                                 'TOOLS.CACHING')
+                request.cached = False
+                request.cacheable = True
+                return False
+        
         if debug:
             cherrypy.log('Reading response from cache', 'TOOLS.CACHING')
         s, h, b, create_time = cache_data
+        age = int(response.time - create_time)
+        if (age > max_age):
+            if debug:
+                cherrypy.log('Ignoring cache due to age > %d' % max_age,
+                             'TOOLS.CACHING')
+            request.cached = False
+            request.cacheable = True
+            return False
         
         # Copy the response headers. See http://www.cherrypy.org/ticket/721.
         response.headers = rh = httputil.HeaderMap()
             dict.__setitem__(rh, k, dict.__getitem__(h, k))
         
         # Add the required Age header
-        response.headers["Age"] = str(int(response.time - create_time))
+        response.headers["Age"] = str(age)
         
         try:
             # Note that validate_since depends on a Last-Modified header;
 
 
 def tee_output():
+    request = cherrypy.serving.request
+    if 'no-store' in request.headers.values('Cache-Control'):
+        return
+    
     def tee(body):
         """Tee response.body into a list."""
+        if ('no-cache' in response.headers.values('Pragma') or
+            'no-store' in response.headers.values('Cache-Control')):
+            for chunk in body:
+                yield chunk
+            return
+        
         output = []
         for chunk in body:
             output.append(chunk)
             yield chunk
         
-        # Might as well do this here; why cache if the body isn't consumed?
-        if response.headers.get('Pragma', None) != 'no-cache':
-            # save the cache data
-            body = ''.join(output)
-            cherrypy._cache.put((response.status, response.headers or {},
-                                 body, response.time), len(body))
+        # save the cache data
+        body = ''.join(output)
+        cherrypy._cache.put((response.status, response.headers or {},
+                             body, response.time), len(body))
     
     response = cherrypy.serving.response
     response.body = tee(response.body)

cherrypy/lib/httputil.py

         value = self.get(key)
         return header_elements(key, value)
     
+    def values(self, key):
+        """Return a sorted list of HeaderElement.value for the given header."""
+        return [e.value for e in self.elements(key)]
+    
     def output(self):
         """Transform self into a list of (name, value) tuples."""
         header_list = []

cherrypy/test/test_caching.py

         _cp_config = {'tools.caching.on': True}
         
         def __init__(self):
-            cherrypy.counter = 0
+            self.counter = 0
+            self.control_counter = 0
             self.longlock = threading.Lock()
         
         def index(self):
-            cherrypy.counter += 1
-            msg = "visit #%s" % cherrypy.counter
+            self.counter += 1
+            msg = "visit #%s" % self.counter
             return msg
         index.exposed = True
         
+        def control(self):
+            self.control_counter += 1
+            return "visit #%s" % self.control_counter
+        control.exposed = True
+        
         def a_gif(self):
             cherrypy.response.headers['Last-Modified'] = httputil.HTTPDate()
             return gif_bytes
         self.assertEqualDates(start, datetime.datetime.now(),
                               # Allow a second for our thread/TCP overhead etc.
                               seconds=SECONDS + 1)
+    
+    def test_cache_control(self):
+        self.getPage("/control")
+        self.assertBody('visit #1')
+        self.getPage("/control")
+        self.assertBody('visit #1')
+        
+        self.getPage("/control", headers=[('Cache-Control', 'no-cache')])
+        self.assertBody('visit #2')
+        self.getPage("/control")
+        self.assertBody('visit #2')
+        
+        self.getPage("/control", headers=[('Pragma', 'no-cache')])
+        self.assertBody('visit #3')
+        self.getPage("/control")
+        self.assertBody('visit #3')
+        
+        time.sleep(1)
+        self.getPage("/control", headers=[('Cache-Control', 'max-age=0')])
+        self.assertBody('visit #4')
+        self.getPage("/control")
+        self.assertBody('visit #4')
+
 
 
 if __name__ == '__main__':