[Checkins] SVN: zope.server/trunk/src/zope/server/http/ Make start_response in the WSGI HTTP Server more compliant with spec (http://www.python.org/dev/peps/pep-0333/)

Satchidanand Haridas satchit at zope.com
Wed May 18 09:33:09 EDT 2011


Log message for revision 121710:
  Make start_response in the WSGI HTTP Server more compliant with spec (http://www.python.org/dev/peps/pep-0333/)
  
  

Changed:
  U   zope.server/trunk/src/zope/server/http/tests/test_wsgiserver.py
  U   zope.server/trunk/src/zope/server/http/wsgihttpserver.py

-=-
Modified: zope.server/trunk/src/zope/server/http/tests/test_wsgiserver.py
===================================================================
--- zope.server/trunk/src/zope/server/http/tests/test_wsgiserver.py	2011-05-18 13:27:49 UTC (rev 121709)
+++ zope.server/trunk/src/zope/server/http/tests/test_wsgiserver.py	2011-05-18 13:33:08 UTC (rev 121710)
@@ -9,9 +9,10 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 ##############################################################################
-"""Test Puvlisher-based HTTP Server
+"""Test Publisher-based HTTP Server
 """
 import StringIO
+import sys
 import unittest
 from asyncore import socket_map, poll
 from threading import Thread
@@ -45,7 +46,14 @@
     Pseudo ZODB conflict error.
     """
 
+ERROR_RESPONSE = "error occurred"
+RESPONSE = "normal response"
 
+class DummyException(Exception):
+    value = "Dummy Exception to test start_response"
+    def __str__(self):
+        return repr(self.value)
+
 class PublicationWithConflict(DefaultPublication):
 
     def handleException(self, object, request, exc_info, retry_allowed=1):
@@ -304,13 +312,15 @@
         self.server.application = app
 
         class FakeTask:
+            wrote_header = 0
             counter = 0
             getCGIEnvironment = lambda _: {}
             class request_data:
                 getBodyStream = lambda _: StringIO.StringIO()
             request_data = request_data()
             setResponseStatus = appendResponseHeaders = lambda *_: None
-
+            def wroteResponseHeader(self):
+                return self.wrote_header
             def write(self, v):
                 self.counter += 1
 
@@ -320,7 +330,88 @@
 
         self.server.application = orig_app
 
+    def _getFakeAppAndTask(self):
 
+        def app(environ, start_response):
+            try:
+                raise DummyException()
+            except DummyException as e:
+                start_response(
+                    '500 Internal Error',
+                    [('Content-type', 'text/plain')],
+                    sys.exc_info())
+                return ERROR_RESPONSE.split()
+            return RESPONSE.split()
+
+        class FakeTask:
+            wrote_header = 0
+            status = None
+            reason = None
+            response = []
+            accumulated_headers = None
+            def __init__(self):
+                self.accumulated_headers = []
+                self.response_headers = {}
+            getCGIEnvironment = lambda _: {}
+            class request_data:
+                getBodyStream = lambda _: StringIO.StringIO()
+            request_data = request_data()
+            def appendResponseHeaders(self, lst):
+                accum = self.accumulated_headers
+                if accum is None:
+                    self.accumulated_headers = accum = []
+                accum.extend(lst)
+            def setResponseStatus(self, status, reason):
+                self.status = status
+                self.reason = reason
+            def wroteResponseHeader(self):
+                return self.wrote_header
+            def write(self, v):
+                self.response.append(v)
+
+        return app, FakeTask()
+
+
+    def test_start_response_with_no_headers_sent(self):
+        # start_response exc_info if no headers have been sent
+        orig_app = self.server.application
+        self.server.application, task = self._getFakeAppAndTask()
+        task.accumulated_headers = ['header1', 'header2']
+        task.accumulated_headers = {'key1': 'value1', 'key2': 'value2'}
+
+        self.server.executeRequest(task)
+
+        self.assertEqual(task.status, "500")
+        self.assertEqual(task.response, ERROR_RESPONSE.split())
+        # any headers written before are cleared and
+        # only the most recent one is added.
+        self.assertEqual(task.accumulated_headers, ['Content-type: text/plain'])
+        # response headers are cleared. They'll be rebuilt from
+        # accumulated_headers in the prepareResponseHeaders method
+        self.assertEqual(task.response_headers, {})
+
+        self.server.application = orig_app
+
+
+    def test_multiple_start_response_calls(self):
+        # if start_response is called more than once with no exc_info
+        ignore, task = self._getFakeAppAndTask()
+        task.wrote_header = 1
+
+        self.assertRaises(AssertionError, self.server.executeRequest, task)
+
+
+    def test_start_response_with_headers_sent(self):
+        # If headers have been sent it raises the exception
+        orig_app = self.server.application
+        self.server.application, task = self._getFakeAppAndTask()
+
+        # If headers have already been written an exception is raised
+        task.wrote_header = 1
+        self.assertRaises(DummyException, self.server.executeRequest, task)
+
+        self.server.application = orig_app
+
 class PMDBTests(Tests):
 
     def _getServerClass(self):
@@ -339,7 +430,50 @@
                               'wsgi.multiprocess', 'wsgi.handleErrors',
                               'wsgi.run_once']))
 
+    def test_multiple_start_response_calls(self):
+        # if start_response is called more than once with no exc_info
+        ignore, task = self._getFakeAppAndTask()
+        task.wrote_header = 1
 
+        # monkey-patch pdb.post_mortem so we don't go into pdb session.
+        pm_traceback = []
+        def fake_post_mortem(tb):
+            import traceback
+            pm_traceback.extend(traceback.format_tb(tb))
+
+        import pdb
+        orig_post_mortem = pdb.post_mortem
+        pdb.post_mortem = fake_post_mortem
+
+        self.assertRaises(AssertionError, self.server.executeRequest, task)
+        expected_msg = "start_response called a second time"
+        self.assertTrue(expected_msg in pm_traceback[-1])
+        pdb.post_mortem = orig_post_mortem
+
+    def test_start_response_with_headers_sent(self):
+        # If headers have been sent it raises the exception, which will
+        # be caught by the server and invoke pdb.post_mortem.
+        orig_app = self.server.application
+        self.server.application, task = self._getFakeAppAndTask()
+        task.wrote_header = 1
+
+        # monkey-patch pdb.post_mortem so we don't go into pdb session.
+        pm_traceback = []
+        def fake_post_mortem(tb):
+            import traceback
+            pm_traceback.extend(traceback.format_tb(tb))
+
+        import pdb
+        orig_post_mortem = pdb.post_mortem
+        pdb.post_mortem = fake_post_mortem
+
+        self.assertRaises(DummyException, self.server.executeRequest, task)
+        self.assertTrue("raise DummyException" in pm_traceback[-1])
+
+        self.server.application = orig_app
+        pdb.post_mortem = orig_post_mortem
+
+
 def test_suite():
     return unittest.TestSuite((
         unittest.makeSuite(Tests),

Modified: zope.server/trunk/src/zope/server/http/wsgihttpserver.py
===================================================================
--- zope.server/trunk/src/zope/server/http/wsgihttpserver.py	2011-05-18 13:27:49 UTC (rev 121709)
+++ zope.server/trunk/src/zope/server/http/wsgihttpserver.py	2011-05-18 13:33:08 UTC (rev 121710)
@@ -26,6 +26,35 @@
         "Zope 3's HTTP Server does not support the WSGI write() function.")
 
 
+def curriedStartResponse(task):
+    def start_response(status, headers, exc_info=None):
+        if task.wroteResponseHeader() and not exc_info:
+            raise AssertionError("start_response called a second time "
+                                 "without providing exc_info.")
+        if exc_info:
+            try:
+                if task.wroteResponseHeader():
+                    # higher levels will catch and handle raised exception:
+                    # 1. "service" method in httptask.py
+                    # 2. "service" method in severchannelbase.py
+                    # 3. "handlerThread" method in taskthreads.py
+                    raise exc_info[0], exc_info[1], exc_info[2]
+                else:
+                    # As per WSGI spec existing headers must be cleared
+                    task.accumulated_headers = None
+                    task.response_headers = {}
+            finally:
+                exc_info = None
+        # Prepare the headers for output
+        status, reason = re.match('([0-9]*) (.*)', status).groups()
+        task.setResponseStatus(status, reason)
+        task.appendResponseHeaders(['%s: %s' % i for i in headers])
+
+        # Return the write method used to write the response data.
+        return fakeWrite
+    return start_response
+
+
 class WSGIHTTPServer(HTTPServer):
     """Zope Publisher-specific WSGI-compliant HTTP Server"""
 
@@ -76,17 +105,9 @@
         """Overrides HTTPServer.executeRequest()."""
         env = self._constructWSGIEnvironment(task)
 
-        def start_response(status, headers):
-            # Prepare the headers for output
-            status, reason = re.match('([0-9]*) (.*)', status).groups()
-            task.setResponseStatus(status, reason)
-            task.appendResponseHeaders(['%s: %s' % i for i in headers])
+        # Call the application to handle the request and write a response
+        result = self.application(env, curriedStartResponse(task))
 
-            # Return the write method used to write the response data.
-            return fakeWrite
-
-        # Call the application to handle the request and write a response
-        result = self.application(env, start_response)
         # By iterating manually at this point, we execute task.write()
         # multiple times, allowing partial data to be sent.
         for value in result:
@@ -101,18 +122,9 @@
         env = self._constructWSGIEnvironment(task)
         env['wsgi.handleErrors'] = False
 
-        def start_response(status, headers):
-            # Prepare the headers for output
-            status, reason = re.match('([0-9]*) (.*)', status).groups()
-            task.setResponseStatus(status, reason)
-            task.appendResponseHeaders(['%s: %s' % i for i in headers])
-
-            # Return the write method used to write the response data.
-            return fakeWrite
-
         # Call the application to handle the request and write a response
         try:
-            result = self.application(env, start_response)
+            result = self.application(env, curriedStartResponse(task))
             # By iterating manually at this point, we execute task.write()
             # multiple times, allowing partial data to be sent.
             for value in result:
@@ -137,5 +149,5 @@
     task_dispatcher = ThreadedTaskDispatcher()
     task_dispatcher.setThreadCount(threads)
     server = WSGIHTTPServer(wsgi_app, name, host, port,
-                            task_dispatcher=task_dispatcher)    
+                            task_dispatcher=task_dispatcher)
     asyncore.loop()



More information about the checkins mailing list