Commits

Anonymous committed 4bfb3e1

2.x backport of fixes in [1388] and [1530] (safer WSGI and request close). See #567.

Also buglet fixes in test.py and helper.py.

Comments (0)

Files changed (6)

cherrypy/_cpwsgi.py

     def write(self, data):
         pass
 
+class ResponseIter(object):
+    def __init__(self, request, body):
+        self.body = body
+        self.request = request
+        
+    def __iter__(self):
+        if not self.body:
+            raise StopIteration
+        try:
+            for chunk in self.body:
+                # WSGI requires all data to be of type "str". This coercion should
+                # not take any time at all if chunk is already of type "str".
+                # If it's unicode, it could be a big performance hit (x ~500).
+                if not isinstance(chunk, str):
+                    chunk = chunk.encode("ISO-8859-1")
+                yield chunk
+        except (KeyboardInterrupt, SystemExit), ex:
+            raise ex
+        except:
+            cherrypy.log(traceback=True)
+            s, h, b = _cputil.bareError()
+            # CherryPy test suite expects bareError body to be output,
+            # so don't call start_response (which, according to PEP 333,
+            # may raise its own error at that point).
+            for chunk in b:
+                # WSGI requires all data to be of type "str". This coercion should
+                # not take any time at all if chunk is already of type "str".
+                # If it's unicode, it could be a big performance hit (x ~500).
+                if not isinstance(chunk, str):
+                    chunk = chunk.encode("ISO-8859-1")
+                yield chunk
+    
+    def close(self):
+        try:
+            if self.request:
+                self.request.close()
+        except:
+            cherrypy.log(traceback=True)
+        self.request = None
 
 def wsgiApp(environ, start_response):
     """The WSGI 'application object' for CherryPy."""
         raise ex 
     except:
         if cherrypy.config.get("server.throw_errors", False):
+            if request:
+                request.close()
+                request = None
             raise
         tb = _cputil.formatExc()
         cherrypy.log(tb)
         s, h, b = _cputil.bareError(tb)
         exc = sys.exc_info()
     
-    try:
-        start_response(s, h, exc)
-        for chunk in b:
-            # WSGI requires all data to be of type "str". This coercion should
-            # not take any time at all if chunk is already of type "str".
-            # If it's unicode, it could be a big performance hit (x ~500).
-            if not isinstance(chunk, str):
-                chunk = chunk.encode("ISO-8859-1")
-            yield chunk
-        if request:
-            request.close()
-        request = None
-    except (KeyboardInterrupt, SystemExit), ex:
-        try:
-            if request:
-                request.close()
-        except:
-            cherrypy.log(traceback=True)
-        request = None
-        raise ex
-    except:
-        cherrypy.log(traceback=True)
-        try:
-            if request:
-                request.close()
-        except:
-            cherrypy.log(traceback=True)
-        request = None
-        s, h, b = _cputil.bareError()
-        # CherryPy test suite expects bareError body to be output,
-        # so don't call start_response (which, according to PEP 333,
-        # may raise its own error at that point).
-        for chunk in b:
-            if not isinstance(chunk, str):
-                chunk = chunk.encode("ISO-8859-1")
-            yield chunk
+    start_response(s, h, exc)
+    return ResponseIter(request, b)
+
+
 
 
 # Server components.
         self.ssl_certificate = conf("server.ssl_certificate")
         self.ssl_private_key = conf("server.ssl_private_key")
 
+

cherrypy/_cpwsgiserver.py

                         if request.ready:
                             response = request.wsgi_app(request.environ,
                                                         request.start_response)
-                            for line in response:
-                                request.write(line)
-                            if hasattr(response, "close"):
-                                response.close()
+                            try:
+                                for line in response:
+                                    request.write(line)
+                            finally:
+                                if hasattr(response, "close"):
+                                    response.close()
                     except socket.error, e:
                         errno = e.args[0]
                         if errno not in socket_errors_to_ignore:
                 except AssertionError:
                     pass
 
+

cherrypy/filters/wsgiappfilter.py

         # constructor
         environ.update(self.env_update)
 
-        # run the wsgi app and have it set response.body
-        cherrypy.response.body = self.app(environ, start_response)
+        # run the wsgi app and have it set response.body
+        response = self.app(environ, start_response)
+        try:
+            cherrypy.response.body = response
+        finally:
+            if hasattr(response, "close"):
+                response.close()
         
         # tell CP not to handle the request further
         request.execute_main = False

cherrypy/test/helper.py

 class CPWebCase(webtest.WebCase):
     
     mount_point = ""
+    scheme = "http"
     
     def prefix(self):
         return self.mount_point.rstrip("/")

cherrypy/test/test.py

         print """
     
     --port=<int>: use a port other than the default (%s).
-    --1.1: use HTTP/1.1 servers instead of default HTTP/1.0.
+    --1.0: use HTTP/1.0 servers instead of default HTTP/1.1.
     
     --cover: turn on the code-coverage tool.
     --basedir=path: display coverage stats for some path other than cherrypy.

cherrypy/test/test_custom_filters.py

 """Test the various means of instantiating and invoking filters."""
 
+import time
 import types
 import test
 test.prefer_parent_path()
         def err(self):
             raise ValueError()
         
+        def stream(self):
+            for i in xrange(100000000):
+                yield str(i)
+        
         def errinstream(self):
             raise ValueError()
             yield "confidential"
         '/cpfilterlist/errinstream': {
             'stream_response': True,
         },
+        '/cpfilterlist/stream': {
+            'stream_response': True,
+        },
         '/cpfilterlist/err_in_onstart': {
             # Because this isn't a dict, on_start_resource will error.
             'numerify_filter.map': "pie->3.14159"
 
 
 class FilterTests(helper.CPWebCase):
-    
     def testCPFilterList(self):
         self.getPage("/cpfilterlist/")
         # If body is "razdrez", then on_end_request is being called too early.
         # If this fails, then on_end_request isn't being called at all.
         self.getPage("/cpfilterlist/ended/5")
         self.assertBody("True")
+
+        # Test that on_end_request is called even if the client drops.
+        self.persistent = True
+        try:
+            conn = self.HTTP_CONN
+            conn.putrequest('GET', '/cpfilterlist/stream', skip_host=True)
+            conn.putheader('Host', self.HOST)
+            conn.endheaders()
+            # Skip the rest of the request and close the conn. This will 
+            # cause the server's active socket to error, which *should* 
+            # result in the request being aborted, and request.close being 
+            # called all the way up the stack (including WSGI middleware), 
+            # eventually calling our on_end_request hook.
+        finally:
+            self.persistent = False
+        time.sleep(0.1)
+        # on_end_request should have been called
+        self.getPage('/cpfilterlist/ended/7')
+        self.assertBody("True")
         
         # Test the config method.
         self.getPage("/cpfilterlist/restricted")
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 ProjectModifiedEvent.java.
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.