Index: Lib/test/test_xmlrpc.py =================================================================== --- Lib/test/test_xmlrpc.py (revision 73271) +++ Lib/test/test_xmlrpc.py (working copy) @@ -273,7 +273,7 @@ # The evt is set twice. First when the server is ready to serve. # Second when the server has been shutdown. The user must clear # the event after it has been set the first time to catch the second set. -def http_server(evt, numrequests): +def http_server(evt, numrequests, requestHandler=None): class TestInstanceClass: def div(self, x, y): return x // y @@ -294,7 +294,9 @@ s.setblocking(True) return s, port - serv = MyXMLRPCServer(("localhost", 0), + if not requestHandler: + requestHandler = SimpleXMLRPCServer.SimpleXMLRPCRequestHandler + serv = MyXMLRPCServer(("localhost", 0), requestHandler, logRequests=False, bind_and_activate=False) try: serv.socket.settimeout(3) @@ -348,34 +350,36 @@ return False -# NOTE: The tests in SimpleServerTestCase will ignore failures caused by -# "temporarily unavailable" exceptions raised in SimpleXMLRPCServer. This -# condition occurs infrequently on some platforms, frequently on others, and -# is apparently caused by using SimpleXMLRPCServer with a non-blocking socket. -# If the server class is updated at some point in the future to handle this -# situation more gracefully, these tests should be modified appropriately. - -class SimpleServerTestCase(unittest.TestCase): +class BaseServerTestCase(unittest.TestCase): + requestHandler = None def setUp(self): # enable traceback reporting SimpleXMLRPCServer.SimpleXMLRPCServer._send_traceback_header = True self.evt = threading.Event() # start server thread to handle requests - serv_args = (self.evt, 1) + serv_args = (self.evt, 1, self.requestHandler) threading.Thread(target=http_server, args=serv_args).start() # wait for the server to be ready - self.evt.wait() + self.evt.wait(10) self.evt.clear() def tearDown(self): # wait on the server thread to terminate - self.evt.wait() + self.evt.wait(10) # disable traceback reporting SimpleXMLRPCServer.SimpleXMLRPCServer._send_traceback_header = False +# NOTE: The tests in SimpleServerTestCase will ignore failures caused by +# "temporarily unavailable" exceptions raised in SimpleXMLRPCServer. This +# condition occurs infrequently on some platforms, frequently on others, and +# is apparently caused by using SimpleXMLRPCServer with a non-blocking socket. +# If the server class is updated at some point in the future to handle this +# situation more gracefully, these tests should be modified appropriately. + +class SimpleServerTestCase(BaseServerTestCase): def test_simple1(self): try: p = xmlrpclib.ServerProxy(URL) @@ -512,6 +516,37 @@ # This avoids waiting for the socket timeout. self.test_simple1() +#A test case that verifies that a server using the HTTP/1.1 keep-alive mechanism +#does indeed serve subsequent requests on the same connection +class KeepaliveServerTestCase(BaseServerTestCase): + #a request handler that supports keep-alive and logs requests into a + #class variable + class RequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): + parentClass = SimpleXMLRPCServer.SimpleXMLRPCRequestHandler + protocol_version = 'HTTP/1.1' + myRequests = [] + def handle(self): + self.myRequests.append([]) + return self.parentClass.handle(self) + def handle_one_request(self): + result = self.parentClass.handle_one_request(self) + self.myRequests[-1].append(self.raw_requestline) + return result + + requestHandler = RequestHandler + def setUp(self): + #clear request log + self.RequestHandler.myRequests = [] + return BaseServerTestCase.setUp(self) + + def test_two(self): + p = xmlrpclib.ServerProxy(URL) + self.assertEqual(p.pow(6,8), 6**8) + self.assertEqual(p.pow(6,8), 6**8) + self.assertEqual(len(self.RequestHandler.myRequests), 1) + #we may or may not catch the final "append" with the empty line + self.failUnless(len(self.RequestHandler.myRequests[-1]) >= 2) + # This is a contrived way to make a failure occur on the server side # in order to test the _send_traceback_header flag on the server class FailingMessageClass(mimetools.Message): @@ -703,7 +738,7 @@ def make_connection(self, host): conn = xmlrpclib.Transport.make_connection(self, host) - conn._conn.sock = self.fake_socket = FakeSocket() + conn.sock = self.fake_socket = FakeSocket() return conn class TransportSubclassTestCase(unittest.TestCase): @@ -729,15 +764,15 @@ req = self.issue_request(TestTransport) self.assert_("X-Test: test_custom_user_agent\r\n" in req) - def test_send_host(self): + def test_send_extra_headers(self): class TestTransport(FakeTransport): - def send_host(self, conn, host): - xmlrpclib.Transport.send_host(self, conn, host) - conn.putheader("X-Test", "test_send_host") + def send_extra_headers(self, conn): + xmlrpclib.Transport.send_extra_headers(self, conn) + conn.putheader("X-Test", "test_extra_headers") req = self.issue_request(TestTransport) - self.assert_("X-Test: test_send_host\r\n" in req) + self.assert_("X-Test: test_extra_headers\r\n" in req) def test_send_request(self): class TestTransport(FakeTransport): @@ -763,6 +798,7 @@ xmlrpc_tests = [XMLRPCTestCase, HelperTestCase, DateTimeTestCase, BinaryTestCase, FaultTestCase, TransportSubclassTestCase] xmlrpc_tests.append(SimpleServerTestCase) + xmlrpc_tests.append(KeepaliveServerTestCase) xmlrpc_tests.append(FailingServerTestCase) xmlrpc_tests.append(CGIHandlerTestCase) Index: Lib/xmlrpclib.py =================================================================== --- Lib/xmlrpclib.py (revision 73271) +++ Lib/xmlrpclib.py (working copy) @@ -140,6 +140,9 @@ from types import * +import socket +import errno + # -------------------------------------------------------------------- # Internal stuff @@ -1158,7 +1161,27 @@ def __init__(self, use_datetime=0): self._use_datetime = use_datetime + self._connection = None + self._extra_headers = [] + ## + # Send a complete request, and parse the response. + # Retry request if a cached connection has disconnected. + # + # @param host Target host. + # @param handler Target PRC handler. + # @param request_body XML-RPC request body. + # @param verbose Debugging flag. + # @return Parsed response. + def request(self, host, handler, request_body, verbose=0): + #retry request if cached connection has gone cold + for i in (0, 1): + try: + return self.single_request(host, handler, request_body, verbose) + except socket.error, e: + if not (i == 0 and e[0] == errno.ECONNRESET): + raise + ## # Send a complete request, and parse the response. # @@ -1168,31 +1191,38 @@ # @param verbose Debugging flag. # @return Parsed response. - def request(self, host, handler, request_body, verbose=0): + def single_request(self, host, handler, request_body, verbose=0): # issue XML-RPC request h = self.make_connection(host) if verbose: h.set_debuglevel(1) - self.send_request(h, handler, request_body) - self.send_host(h, host) - self.send_user_agent(h) - self.send_content(h, request_body) + try: + self.send_request(h, handler, request_body) + self.send_extra_headers(h) + self.send_user_agent(h) + self.send_content(h, request_body) - errcode, errmsg, headers = h.getreply(buffering=True) + response = h.getresponse(buffering=True) + if response.status == 200: + self.verbose = verbose + return self.parse_response(response) + except Fault: + raise + except Exception: + # All unexpected errors leave connection in + # a strange state, so we clear it. + self.clear_connection() + raise - if errcode != 200: - raise ProtocolError( - host + handler, - errcode, errmsg, - headers - ) + response.read() #discard any response data + raise ProtocolError( + host + handler, + response.status, response.reason, + response.msg, + ) - self.verbose = verbose - - return self.parse_response(h.getfile()) - ## # Create parser. # @@ -1240,12 +1270,24 @@ # @return A connection handle. def make_connection(self, host): + if self._connection: + return self._connection # create a HTTP connection object from a host descriptor import httplib - host, extra_headers, x509 = self.get_host_info(host) - return httplib.HTTP(host) + host, self._extra_headers, x509 = self.get_host_info(host) + self._connection = httplib.HTTPConnection(host) + return self._connection ## + # Clear any cached connection object. + # Used in the event of socket errors. + # + def clear_connection(self): + if self._connection: + self._connection.close() + self._connection = None + + ## # Send request header. # # @param connection Connection handle. @@ -1256,14 +1298,12 @@ connection.putrequest("POST", handler) ## - # Send host name. + # Send extra headers. # # @param connection Connection handle. - # @param host Host name. - def send_host(self, connection, host): - host, extra_headers, x509 = self.get_host_info(host) - connection.putheader("Host", host) + def send_extra_headers(self, connection): + extra_headers = self._extra_headers if extra_headers: if isinstance(extra_headers, DictType): extra_headers = extra_headers.items() @@ -1295,20 +1335,19 @@ # @param file Stream. # @return Response tuple and target method. - def parse_response(self, file): - # read response from input file/socket, and parse it + def parse_response(self, response): + # read response data from httpresponse, and parse it p, u = self.getparser() while 1: - response = file.read(1024) - if not response: + data = response.read(1024) + if not data: break if self.verbose: - print "body:", repr(response) - p.feed(response) + print "body:", repr(data) + p.feed(data) - file.close() p.close() return u.close() @@ -1322,18 +1361,14 @@ # FIXME: mostly untested def make_connection(self, host): + if self._connection: + return self._connection # create a HTTPS connection object from a host descriptor # host may be a string, or a (host, x509-dict) tuple import httplib - host, extra_headers, x509 = self.get_host_info(host) - try: - HTTPS = httplib.HTTPS - except AttributeError: - raise NotImplementedError( - "your version of httplib doesn't support HTTPS" - ) - else: - return HTTPS(host, None, **(x509 or {})) + host, self._extra_headers, x509 = self.get_host_info(host) + self._connection = httplib.HTTPSConnection(host, None, **(x509 or {})) + return self._connection ## # Standard server proxy. This class establishes a virtual connection