# HG changeset patch # Parent eaa38b75cc7812dbf079801657f6eae9ebb85157 Issue 3566: Improve support for persistent connections * Add http.client.ConnectionClosed exception * HTTPConnection.close() implicitly called * request() ConnectionError does not imply server did not send a response * ConnectionClosed wraps ECONNRESET from first recv() of status line diff -r eaa38b75cc78 Doc/library/http.client.rst --- a/Doc/library/http.client.rst Fri Jan 23 17:30:26 2015 -0500 +++ b/Doc/library/http.client.rst Tue Feb 17 06:23:18 2015 +0000 @@ -169,6 +169,16 @@ status code that we don't understand. +.. exception:: RemoteDisconnected + + A subclass of :exc:`ConnectionError`, and also :exc:`BadStatusLine` for + backwards compatibility. Raised by :meth:`HTTPConnection.getresponse` + when a connection is shut down before any response is received. + + .. versionadded:: 3.5 + Previously, :exc:`BadStatusLine` was raised. + + The constants defined in this module are: .. data:: HTTP_PORT @@ -231,6 +241,11 @@ Note that you must have read the whole response before you can send a new request to the server. + .. versionchanged:: 3.5 + If a :exc:`ConnectionError` or subclass is raised, the + :class:`HTTPConnection` object connection will be ready to reconnect + when a new request is sent. + .. method:: HTTPConnection.set_debuglevel(level) @@ -269,7 +284,9 @@ .. method:: HTTPConnection.connect() - Connect to the server specified when the object was created. + Connect to the server specified when the object was created. This is + called automatically when making a request if the client does not already + have a connection ready. .. method:: HTTPConnection.close() diff -r eaa38b75cc78 Lib/http/client.py --- a/Lib/http/client.py Fri Jan 23 17:30:26 2015 -0500 +++ b/Lib/http/client.py Tue Feb 17 06:23:18 2015 +0000 @@ -20,10 +20,12 @@ | ( putheader() )* endheaders() v Request-sent - | - | response = getresponse() - v - Unread-response [Response-headers-read] + |\_____________________________ + | | getresponse() raises + | response = getresponse() | ConnectionError + v v + Unread-response Idle + [Response-headers-read] |\____________________ | | | response.read() | putrequest() @@ -80,7 +82,7 @@ "UnknownTransferEncoding", "UnimplementedFileMode", "IncompleteRead", "InvalidURL", "ImproperConnectionState", "CannotSendRequest", "CannotSendHeader", "ResponseNotReady", - "BadStatusLine", "error", "responses"] + "BadStatusLine", "RemoteDisconnected", "error", "responses"] HTTP_PORT = 80 HTTPS_PORT = 443 @@ -208,9 +210,9 @@ if self.debuglevel > 0: print("reply:", repr(line)) if not line: - # Presumably, the server closed the connection before + # Presumably, the server shut the connection down before # sending a valid response. - raise BadStatusLine(line) + raise RemoteDisconnected("Connection shut down without response") try: version, status, reason = line.split(None, 2) except ValueError: @@ -1107,7 +1109,11 @@ response = self.response_class(self.sock, method=self._method) try: - response.begin() + try: + response.begin() + except ConnectionError: + self.close() + raise assert response.will_close != _UNKNOWN self.__state = _CS_IDLE @@ -1239,5 +1245,10 @@ HTTPException.__init__(self, "got more than %d bytes when reading %s" % (_MAXLINE, line_type)) +class RemoteDisconnected(ConnectionError, BadStatusLine): + def __init__(self, *pos, **kw): + BadStatusLine.__init__(self, "") + ConnectionError.__init__(self, *pos, **kw) + # for backwards compatibility error = HTTPException diff -r eaa38b75cc78 Lib/test/test_httplib.py --- a/Lib/test/test_httplib.py Fri Jan 23 17:30:26 2015 -0500 +++ b/Lib/test/test_httplib.py Tue Feb 17 06:23:18 2015 +0000 @@ -106,6 +106,23 @@ raise AssertionError('caller tried to read past EOF') return data +class FakeSocketHTTPConnection(client.HTTPConnection): + """HTTPConnection subclass using FakeSocket; counts connect() calls""" + + def __init__(self, *args): + self.connections = 0 + super().__init__('example.com') + self.fake_socket_args = args + self._create_connection = self.create_connection + + def connect(self): + """Count the number of times connect() is invoked""" + self.connections += 1 + return super().connect() + + def create_connection(self, *pos, **kw): + return FakeSocket(*self.fake_socket_args) + class HeaderTests(TestCase): def test_auto_headers(self): # Some headers are added automatically, but should not be added by @@ -671,7 +688,7 @@ response = self # Avoid garbage collector closing the socket client.HTTPResponse.__init__(self, *pos, **kw) conn.response_class = Response - conn.sock = FakeSocket('') # Emulate server dropping connection + conn.sock = FakeSocket('Invalid status line') conn.request('GET', '/') self.assertRaises(client.BadStatusLine, conn.getresponse) self.assertTrue(response.closed) @@ -992,6 +1009,85 @@ httpConn.close() +class PersistenceTest(TestCase): + def test_reuse_reconnect(self): + # Should reuse or reconnect depending on header from server + tests = ( + ('1.0', None, False), + ('1.0', 'keep-alive', True), + ('1.0', 'keep-ALIVE', True), # Test case insensitivity + ('1.1', None, True), + ('1.1', 'close', False), + ('1.1', 'cloSE', False), # Test case insensitivity + ) + for version, header, reuse in tests: + with self.subTest(version=version, header=header): + if header: + header = 'Connection: {}\r\n'.format(header) + else: + header = '' + msg = ( + 'HTTP/{} 200 OK\r\n' + '{}' + 'Content-Length: 12\r\n' + '\r\n' + 'Dummy body\r\n' + ) + msg = msg.format(version, header) + conn = FakeSocketHTTPConnection(msg) + self.assertIsNone(conn.sock) + conn.request('GET', '/open-connection') + with conn.getresponse() as response: + self.assertEqual(conn.sock is None, not reuse) + response.read() + self.assertEqual(conn.sock is None, not reuse) + self.assertEqual(conn.connections, 1) + conn.request('GET', '/subsequent-request') + if reuse: + self.assertEqual(conn.connections, 1) + else: + self.assertEqual(conn.connections, 2) + + def test_disconnected(self): + def make_reset_reader(text): + """Return BufferedReader that raises ECONNRESET at EOF""" + stream = io.BytesIO(text) + def readinto(buffer): + size = io.BytesIO.readinto(stream, buffer) + if size == 0: + raise ConnectionResetError() + return size + stream.readinto = readinto + return io.BufferedReader(stream) + + tests = ( + (io.BytesIO, client.RemoteDisconnected), + (make_reset_reader, ConnectionResetError), + ) + for stream_factory, exception in tests: + with self.subTest(exception): + conn = FakeSocketHTTPConnection(b'', stream_factory) + conn.request('GET', '/eof-response') + self.assertRaises(exception, conn.getresponse) + self.assertIsNone(conn.sock) + + # HTTPConnection.connect() should be automatically invoked + conn.request('GET', '/reconnect') + self.assertEqual(conn.connections, 2) + + def test_100_close(self): + conn = FakeSocketHTTPConnection( + b'HTTP/1.1 100 Continue\r\n' + b'\r\n' + # Missing final response + ) + conn.request('GET', '/', headers={'Expect': '100-continue'}) + self.assertRaises(client.RemoteDisconnected, conn.getresponse) + self.assertIsNone(conn.sock) + conn.request('GET', '/reconnect') + self.assertEqual(conn.connections, 2) + + class HTTPSTest(TestCase): def setUp(self): @@ -1298,6 +1394,7 @@ @support.reap_threads def test_main(verbose=None): support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest, + PersistenceTest, HTTPSTest, RequestBodyTest, SourceAddressTest, HTTPResponseTest, ExtendedReadTest, ExtendedReadTestChunked, TunnelTests) diff -r eaa38b75cc78 Lib/xmlrpc/client.py --- a/Lib/xmlrpc/client.py Fri Jan 23 17:30:26 2015 -0500 +++ b/Lib/xmlrpc/client.py Tue Feb 17 06:23:18 2015 +0000 @@ -1143,7 +1143,7 @@ if i or e.errno not in (errno.ECONNRESET, errno.ECONNABORTED, errno.EPIPE): raise - except http.client.BadStatusLine: #close after we sent request + except http.client.RemoteDisconnected: if i: raise