summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/http.client.rst20
-rw-r--r--Lib/http/client.py27
-rw-r--r--Lib/test/test_httplib.py92
-rw-r--r--Lib/xmlrpc/client.py2
4 files changed, 131 insertions, 10 deletions
diff --git a/Doc/library/http.client.rst b/Doc/library/http.client.rst
index a7af016..5e2d68c 100644
--- a/Doc/library/http.client.rst
+++ b/Doc/library/http.client.rst
@@ -175,6 +175,17 @@ The following exceptions are raised as appropriate:
is received in the HTTP protocol from the server.
+.. exception:: RemoteDisconnected
+
+ A subclass of :exc:`ConnectionResetError` and :exc:`BadStatusLine`. Raised
+ by :meth:`HTTPConnection.getresponse` when the attempt to read the response
+ results in no data read from the connection, indicating that the remote end
+ has closed the connection.
+
+ .. versionadded:: 3.5
+ Previously, :exc:`BadStatusLine`\ ``('')`` was raised.
+
+
The constants defined in this module are:
.. data:: HTTP_PORT
@@ -247,6 +258,11 @@ HTTPConnection Objects
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 will be ready to reconnect when
+ a new request is sent.
+
.. method:: HTTPConnection.set_debuglevel(level)
@@ -285,7 +301,9 @@ HTTPConnection Objects
.. method:: HTTPConnection.connect()
- Connect to the server specified when the object was created.
+ Connect to the server specified when the object was created. By default,
+ this is called automatically when making a request if the client does not
+ already have a connection.
.. method:: HTTPConnection.close()
diff --git a/Lib/http/client.py b/Lib/http/client.py
index f5889ed..ac120e7 100644
--- a/Lib/http/client.py
+++ b/Lib/http/client.py
@@ -20,10 +20,12 @@ request. This diagram details these state transitions:
| ( 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()
@@ -83,7 +85,8 @@ __all__ = ["HTTPResponse", "HTTPConnection",
"UnknownTransferEncoding", "UnimplementedFileMode",
"IncompleteRead", "InvalidURL", "ImproperConnectionState",
"CannotSendRequest", "CannotSendHeader", "ResponseNotReady",
- "BadStatusLine", "LineTooLong", "error", "responses"]
+ "BadStatusLine", "LineTooLong", "RemoteDisconnected", "error",
+ "responses"]
HTTP_PORT = 80
HTTPS_PORT = 443
@@ -245,7 +248,8 @@ class HTTPResponse(io.BufferedIOBase):
if not line:
# Presumably, the server closed the connection before
# sending a valid response.
- raise BadStatusLine(line)
+ raise RemoteDisconnected("Remote end closed connection without"
+ " response")
try:
version, status, reason = line.split(None, 2)
except ValueError:
@@ -1160,7 +1164,11 @@ class HTTPConnection:
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
@@ -1292,5 +1300,10 @@ class LineTooLong(HTTPException):
HTTPException.__init__(self, "got more than %d bytes when reading %s"
% (_MAXLINE, line_type))
+class RemoteDisconnected(ConnectionResetError, BadStatusLine):
+ def __init__(self, *pos, **kw):
+ BadStatusLine.__init__(self, "")
+ ConnectionResetError.__init__(self, *pos, **kw)
+
# for backwards compatibility
error = HTTPException
diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py
index b4ec63c..d809414 100644
--- a/Lib/test/test_httplib.py
+++ b/Lib/test/test_httplib.py
@@ -107,6 +107,23 @@ class NoEOFBytesIO(io.BytesIO):
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
@@ -777,7 +794,7 @@ class BasicTest(TestCase):
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)
@@ -1174,6 +1191,78 @@ class TimeoutTest(TestCase):
httpConn.close()
+class PersistenceTest(TestCase):
+
+ def test_reuse_reconnect(self):
+ # Should reuse or reconnect depending on header from server
+ tests = (
+ ('1.0', '', False),
+ ('1.0', 'Connection: keep-alive\r\n', True),
+ ('1.1', '', True),
+ ('1.1', 'Connection: close\r\n', False),
+ ('1.0', 'Connection: keep-ALIVE\r\n', True),
+ ('1.1', 'Connection: cloSE\r\n', False),
+ )
+ for version, header, reuse in tests:
+ with self.subTest(version=version, header=header):
+ msg = (
+ 'HTTP/{} 200 OK\r\n'
+ '{}'
+ 'Content-Length: 12\r\n'
+ '\r\n'
+ 'Dummy body\r\n'
+ ).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')
+ self.assertEqual(conn.connections, 1 if reuse else 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=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):
@@ -1513,6 +1602,7 @@ class TunnelTests(TestCase):
@support.reap_threads
def test_main(verbose=None):
support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest,
+ PersistenceTest,
HTTPSTest, RequestBodyTest, SourceAddressTest,
HTTPResponseTest, ExtendedReadTest,
ExtendedReadTestChunked, TunnelTests)
diff --git a/Lib/xmlrpc/client.py b/Lib/xmlrpc/client.py
index 34208d1..da089a2 100644
--- a/Lib/xmlrpc/client.py
+++ b/Lib/xmlrpc/client.py
@@ -1128,7 +1128,7 @@ class Transport:
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