[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