Commits

Lynn Rees committed 3ea5f29

- do not cache non cacheable headers (bug reported by
Roberto De Alemeida)
- non-destructively copy wsgi.input (from wsgiform) (bug reported by
Roberto De Alemeida)

  • Participants
  • Parent commits ebb2719
  • Branches wsgistate

Comments (0)

Files changed (1)

File trunk/wsgistate/cache.py

 # Redistribution and use in source and binary forms, with or without modification,
 # are permitted provided that the following conditions are met:
 #
-#    1. Redistributions of source code must retain the above copyright notice, 
+#    1. Redistributions of source code must retain the above copyright notice,
 #       this list of conditions and the following disclaimer.
-#    
-#    2. Redistributions in binary form must reproduce the above copyright 
+#
+#    2. Redistributions in binary form must reproduce the above copyright
 #       notice, this list of conditions and the following disclaimer in the
 #       documentation and/or other materials provided with the distribution.
 #
 
 import time
 import rfc822
-from copy import copy
+from StringIO import StringIO
 
 __all__ = ['WsgiMemoize', 'CacheHeader', 'memoize', 'public', 'private',
     'nocache', 'nostore', 'notransform', 'revalidate', 'proxyrevalidate',
     'maxage', 'smaxage', 'vary', 'modified']
 
+def getinput(environ):
+    '''Non-destructively retrieves wsgi.input value.'''
+    wsginput = environ['wsgi.input']
+    # Non-destructively fetch string value of wsgi.input
+    if hasattr(wsginput, 'getvalue'):
+        qs = wsginput.getvalue()
+    # Otherwise, read and reconstruct wsgi.input
+    else:
+        # Never read more than content length
+        clength = int(environ['CONTENT_LENGTH'])
+        qs = wsginput.read(clength)
+        environ['wsgi.input'] = StringIO(qs)
+    return qs
+
 def expiredate(seconds, value):
     '''Expire date headers for cache control.
 
 
     @param application WSGI application
     @param value 'Cache-Control' value
-    '''    
+    '''
     now = rfc822.formatdate()
     headers = {'Cache-Control':value, 'Date':now, 'Expires':now}
     return CacheHeader(application, headers)
     '''Generic setter for 'Cache-Control' headers + future expiration info.
 
     @param value 'Cache-Control' value
-    @param seconds # of seconds a resource should be considered invalid in   
+    @param seconds # of seconds a resource should be considered invalid in
     '''
     def decorator(application):
         return CacheHeader(application, expiredate(second, value))
 def public(application):
     '''Response MAY be cached.'''
     return control(application, 'public')
-    
+
 def private(application):
     '''Response intended for 1 user that MUST NOT be cached.'''
     return expire(application, 'private')
-    
+
 def nocache(application):
     '''Response that a cache can't send without origin server revalidation.'''
     now = rfc822.formatdate()
     return age('max-age=%d', seconds)
 
 def smaxage(seconds):
-    '''Sets the maximum time in seconds a shared cache can store a response.''' 
+    '''Sets the maximum time in seconds a shared cache can store a response.'''
     return age('s-maxage=%d', seconds)
 
 def expires(seconds):
     def __init__(self, application, headers):
         self.application = application
         self.headers = headers
-        
+
     def __call__(self, environ, start_response):
         # Restrict cache control to GET and HEAD per HTTP 1.1 RFC
         if environ.get('REQUEST_METHOD') in ('GET', 'HEAD'):
             def hdr_response(status, headers, exc_info=None):
                 theaders = self.headers.copy()
                 # Aggregate all 'Cache-Control' directives
-                if 'Cache-Control' in theaders:                    
+                if 'Cache-Control' in theaders:
                     for idx, i in enumerate(headers):
                         if i[0] != 'Cache-Control': continue
                         curval = theaders.pop('Cache-Control')
                 return start_response(status, headers, exc_info)
             return self.application(environ, hdr_response)
         return self.application(environ, start_response)
-        
+
 
 class WsgiMemoize(object):
 
         self._userkey = kw.get('key_user_info', False)
         # Which HTTP responses by method are cached
         self._allowed = kw.get('allowed_methods', set(['GET', 'HEAD']))
-        
+
     def __call__(self, environ, start_response):
+        # Verify requested response is cacheable
+        if environ['REQUEST_METHOD'] not in self._allowed:
+            return self.application(environ, start_response)
         # Generate cache key
         key = self._keygen(environ)
         # Query cache for key prescence
         if info is not None:
             start_response(info['status'], info['headers'], info['exc_info'])
             return info['data']
-        # Verify requested response is cacheable
-        if environ['REQUEST_METHOD'] in self._allowed:
-            # Cache start_response info
-            def cache_response(status, headers, exc_info=None):
-                # Add HTTP cache control headers
-                newhdrs = expiredate(self._cache.timeout, 's-maxage=%d')
-                headers.extend((k, v) for k, v in newhdrs.iteritems())
-                cachedict = {'status':status, 'headers':headers, 'exc_info':exc_info}
-                self._cache.set(key, cachedict)
-                return start_response(status, headers, exc_info)            
-            # Wrap data in list to trigger iterator (Roberto De Alemeida)
-            data = list(self.application(environ, cache_response))
-            # Fetch cached dictionary
-            info = self._cache.get(key)
-            # Store in dictionary
-            info['data'] = data
-            # Store in cache
-            self._cache.set(key, info)
-            # Return data as response to intial request
-            return data
-        return self.application(environ, start_response)
+        # Cache start_response info
+        def cache_response(status, headers, exc_info=None):
+            # Add HTTP cache control headers
+            newhdrs = expiredate(self._cache.timeout, 's-maxage=%d')
+            headers.extend((k, v) for k, v in newhdrs.iteritems())
+            cachedict = {'status':status, 'headers':headers, 'exc_info':exc_info}
+            self._cache.set(key, cachedict)
+            return start_response(status, headers, exc_info)
+        # Wrap data in list to trigger iterator (Roberto De Alemeida)
+        data = list(self.application(environ, cache_response))
+        # Fetch cached dictionary
+        info = self._cache.get(key)
+        # Store in dictionary
+        info['data'] = data
+        # Store in cache
+        self._cache.set(key, info)
+        # Return data as response to intial request
+        return data
 
     def _keygen(self, environ):
         '''Generates cache keys.'''
         if self._methkey: key.append(environ['REQUEST_METHOD'])
         # Add user submitted data to string if configured that way
         if self._userkey:
-            qs = environ.get('QUERY_STRING', '')            
+            qs = environ.get('QUERY_STRING', '')
             if qs != '':
                 key.append(qs)
             else:
-                win = copy(environ['wsgi.input']).read()
+                win = getinput(environ)
                 if win != '': key.append(win)
         return ''.join(key)