Kaydet (Commit) cae7bdb4 authored tarafından R David Murray's avatar R David Murray

#3566: Clean up handling of remote server disconnects.

This changeset does two things: introduces a new RemoteDisconnected exception
(that subclasses ConnectionResetError and BadStatusLine) so that a remote
server disconnection can be detected by client code (and provides a better
error message for debugging purposes), and ensures that the client socket is
closed if a ConnectionError happens, so that the automatic re-connection code
can work if the application handles the error and continues on.

Tests are added that confirm that a connection is re-used or not re-used
as appropriate to the various combinations of protocol version and headers.

Patch by Martin Panter, reviewed by Demian Brecht.  (Tweaked only slightly by
me.)
üst 142bf565
...@@ -175,6 +175,17 @@ The following exceptions are raised as appropriate: ...@@ -175,6 +175,17 @@ The following exceptions are raised as appropriate:
is received in the HTTP protocol from the server. 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: The constants defined in this module are:
.. data:: HTTP_PORT .. data:: HTTP_PORT
...@@ -247,6 +258,11 @@ HTTPConnection Objects ...@@ -247,6 +258,11 @@ HTTPConnection Objects
Note that you must have read the whole response before you can send a new Note that you must have read the whole response before you can send a new
request to the server. 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) .. method:: HTTPConnection.set_debuglevel(level)
...@@ -285,7 +301,9 @@ HTTPConnection Objects ...@@ -285,7 +301,9 @@ HTTPConnection Objects
.. method:: HTTPConnection.connect() .. 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() .. method:: HTTPConnection.close()
......
...@@ -20,10 +20,12 @@ request. This diagram details these state transitions: ...@@ -20,10 +20,12 @@ request. This diagram details these state transitions:
| ( putheader() )* endheaders() | ( putheader() )* endheaders()
v v
Request-sent Request-sent
| |\_____________________________
| response = getresponse() | | getresponse() raises
v | response = getresponse() | ConnectionError
Unread-response [Response-headers-read] v v
Unread-response Idle
[Response-headers-read]
|\____________________ |\____________________
| | | |
| response.read() | putrequest() | response.read() | putrequest()
...@@ -83,7 +85,8 @@ __all__ = ["HTTPResponse", "HTTPConnection", ...@@ -83,7 +85,8 @@ __all__ = ["HTTPResponse", "HTTPConnection",
"UnknownTransferEncoding", "UnimplementedFileMode", "UnknownTransferEncoding", "UnimplementedFileMode",
"IncompleteRead", "InvalidURL", "ImproperConnectionState", "IncompleteRead", "InvalidURL", "ImproperConnectionState",
"CannotSendRequest", "CannotSendHeader", "ResponseNotReady", "CannotSendRequest", "CannotSendHeader", "ResponseNotReady",
"BadStatusLine", "LineTooLong", "error", "responses"] "BadStatusLine", "LineTooLong", "RemoteDisconnected", "error",
"responses"]
HTTP_PORT = 80 HTTP_PORT = 80
HTTPS_PORT = 443 HTTPS_PORT = 443
...@@ -245,7 +248,8 @@ class HTTPResponse(io.BufferedIOBase): ...@@ -245,7 +248,8 @@ class HTTPResponse(io.BufferedIOBase):
if not line: if not line:
# Presumably, the server closed the connection before # Presumably, the server closed the connection before
# sending a valid response. # sending a valid response.
raise BadStatusLine(line) raise RemoteDisconnected("Remote end closed connection without"
" response")
try: try:
version, status, reason = line.split(None, 2) version, status, reason = line.split(None, 2)
except ValueError: except ValueError:
...@@ -1159,8 +1163,12 @@ class HTTPConnection: ...@@ -1159,8 +1163,12 @@ class HTTPConnection:
else: else:
response = self.response_class(self.sock, method=self._method) response = self.response_class(self.sock, method=self._method)
try:
try: try:
response.begin() response.begin()
except ConnectionError:
self.close()
raise
assert response.will_close != _UNKNOWN assert response.will_close != _UNKNOWN
self.__state = _CS_IDLE self.__state = _CS_IDLE
...@@ -1292,5 +1300,10 @@ class LineTooLong(HTTPException): ...@@ -1292,5 +1300,10 @@ class LineTooLong(HTTPException):
HTTPException.__init__(self, "got more than %d bytes when reading %s" HTTPException.__init__(self, "got more than %d bytes when reading %s"
% (_MAXLINE, line_type)) % (_MAXLINE, line_type))
class RemoteDisconnected(ConnectionResetError, BadStatusLine):
def __init__(self, *pos, **kw):
BadStatusLine.__init__(self, "")
ConnectionResetError.__init__(self, *pos, **kw)
# for backwards compatibility # for backwards compatibility
error = HTTPException error = HTTPException
...@@ -107,6 +107,23 @@ class NoEOFBytesIO(io.BytesIO): ...@@ -107,6 +107,23 @@ class NoEOFBytesIO(io.BytesIO):
raise AssertionError('caller tried to read past EOF') raise AssertionError('caller tried to read past EOF')
return data 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): class HeaderTests(TestCase):
def test_auto_headers(self): def test_auto_headers(self):
# Some headers are added automatically, but should not be added by # Some headers are added automatically, but should not be added by
...@@ -777,7 +794,7 @@ class BasicTest(TestCase): ...@@ -777,7 +794,7 @@ class BasicTest(TestCase):
response = self # Avoid garbage collector closing the socket response = self # Avoid garbage collector closing the socket
client.HTTPResponse.__init__(self, *pos, **kw) client.HTTPResponse.__init__(self, *pos, **kw)
conn.response_class = Response conn.response_class = Response
conn.sock = FakeSocket('') # Emulate server dropping connection conn.sock = FakeSocket('Invalid status line')
conn.request('GET', '/') conn.request('GET', '/')
self.assertRaises(client.BadStatusLine, conn.getresponse) self.assertRaises(client.BadStatusLine, conn.getresponse)
self.assertTrue(response.closed) self.assertTrue(response.closed)
...@@ -1174,6 +1191,78 @@ class TimeoutTest(TestCase): ...@@ -1174,6 +1191,78 @@ class TimeoutTest(TestCase):
httpConn.close() 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): class HTTPSTest(TestCase):
def setUp(self): def setUp(self):
...@@ -1513,6 +1602,7 @@ class TunnelTests(TestCase): ...@@ -1513,6 +1602,7 @@ class TunnelTests(TestCase):
@support.reap_threads @support.reap_threads
def test_main(verbose=None): def test_main(verbose=None):
support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest, support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest,
PersistenceTest,
HTTPSTest, RequestBodyTest, SourceAddressTest, HTTPSTest, RequestBodyTest, SourceAddressTest,
HTTPResponseTest, ExtendedReadTest, HTTPResponseTest, ExtendedReadTest,
ExtendedReadTestChunked, TunnelTests) ExtendedReadTestChunked, TunnelTests)
......
...@@ -1128,7 +1128,7 @@ class Transport: ...@@ -1128,7 +1128,7 @@ class Transport:
if i or e.errno not in (errno.ECONNRESET, errno.ECONNABORTED, if i or e.errno not in (errno.ECONNRESET, errno.ECONNABORTED,
errno.EPIPE): errno.EPIPE):
raise raise
except http.client.BadStatusLine: #close after we sent request except http.client.RemoteDisconnected:
if i: if i:
raise raise
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment