Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit cae7bdb

Browse files
committed
#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.)
1 parent 142bf56 commit cae7bdb

4 files changed

Lines changed: 131 additions & 10 deletions

File tree

Doc/library/http.client.rst

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,17 @@ The following exceptions are raised as appropriate:
175175
is received in the HTTP protocol from the server.
176176

177177

178+
.. exception:: RemoteDisconnected
179+
180+
A subclass of :exc:`ConnectionResetError` and :exc:`BadStatusLine`. Raised
181+
by :meth:`HTTPConnection.getresponse` when the attempt to read the response
182+
results in no data read from the connection, indicating that the remote end
183+
has closed the connection.
184+
185+
.. versionadded:: 3.5
186+
Previously, :exc:`BadStatusLine`\ ``('')`` was raised.
187+
188+
178189
The constants defined in this module are:
179190

180191
.. data:: HTTP_PORT
@@ -247,6 +258,11 @@ HTTPConnection Objects
247258
Note that you must have read the whole response before you can send a new
248259
request to the server.
249260

261+
.. versionchanged:: 3.5
262+
If a :exc:`ConnectionError` or subclass is raised, the
263+
:class:`HTTPConnection` object will be ready to reconnect when
264+
a new request is sent.
265+
250266

251267
.. method:: HTTPConnection.set_debuglevel(level)
252268

@@ -285,7 +301,9 @@ HTTPConnection Objects
285301

286302
.. method:: HTTPConnection.connect()
287303

288-
Connect to the server specified when the object was created.
304+
Connect to the server specified when the object was created. By default,
305+
this is called automatically when making a request if the client does not
306+
already have a connection.
289307

290308

291309
.. method:: HTTPConnection.close()

Lib/http/client.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
| ( putheader() )* endheaders()
2121
v
2222
Request-sent
23-
|
24-
| response = getresponse()
25-
v
26-
Unread-response [Response-headers-read]
23+
|\_____________________________
24+
| | getresponse() raises
25+
| response = getresponse() | ConnectionError
26+
v v
27+
Unread-response Idle
28+
[Response-headers-read]
2729
|\____________________
2830
| |
2931
| response.read() | putrequest()
@@ -83,7 +85,8 @@
8385
"UnknownTransferEncoding", "UnimplementedFileMode",
8486
"IncompleteRead", "InvalidURL", "ImproperConnectionState",
8587
"CannotSendRequest", "CannotSendHeader", "ResponseNotReady",
86-
"BadStatusLine", "LineTooLong", "error", "responses"]
88+
"BadStatusLine", "LineTooLong", "RemoteDisconnected", "error",
89+
"responses"]
8790

8891
HTTP_PORT = 80
8992
HTTPS_PORT = 443
@@ -245,7 +248,8 @@ def _read_status(self):
245248
if not line:
246249
# Presumably, the server closed the connection before
247250
# sending a valid response.
248-
raise BadStatusLine(line)
251+
raise RemoteDisconnected("Remote end closed connection without"
252+
" response")
249253
try:
250254
version, status, reason = line.split(None, 2)
251255
except ValueError:
@@ -1160,7 +1164,11 @@ class the response_class variable.
11601164
response = self.response_class(self.sock, method=self._method)
11611165

11621166
try:
1163-
response.begin()
1167+
try:
1168+
response.begin()
1169+
except ConnectionError:
1170+
self.close()
1171+
raise
11641172
assert response.will_close != _UNKNOWN
11651173
self.__state = _CS_IDLE
11661174

@@ -1292,5 +1300,10 @@ def __init__(self, line_type):
12921300
HTTPException.__init__(self, "got more than %d bytes when reading %s"
12931301
% (_MAXLINE, line_type))
12941302

1303+
class RemoteDisconnected(ConnectionResetError, BadStatusLine):
1304+
def __init__(self, *pos, **kw):
1305+
BadStatusLine.__init__(self, "")
1306+
ConnectionResetError.__init__(self, *pos, **kw)
1307+
12951308
# for backwards compatibility
12961309
error = HTTPException

Lib/test/test_httplib.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,23 @@ def readline(self, length=None):
107107
raise AssertionError('caller tried to read past EOF')
108108
return data
109109

110+
class FakeSocketHTTPConnection(client.HTTPConnection):
111+
"""HTTPConnection subclass using FakeSocket; counts connect() calls"""
112+
113+
def __init__(self, *args):
114+
self.connections = 0
115+
super().__init__('example.com')
116+
self.fake_socket_args = args
117+
self._create_connection = self.create_connection
118+
119+
def connect(self):
120+
"""Count the number of times connect() is invoked"""
121+
self.connections += 1
122+
return super().connect()
123+
124+
def create_connection(self, *pos, **kw):
125+
return FakeSocket(*self.fake_socket_args)
126+
110127
class HeaderTests(TestCase):
111128
def test_auto_headers(self):
112129
# Some headers are added automatically, but should not be added by
@@ -777,7 +794,7 @@ def __init__(self, *pos, **kw):
777794
response = self # Avoid garbage collector closing the socket
778795
client.HTTPResponse.__init__(self, *pos, **kw)
779796
conn.response_class = Response
780-
conn.sock = FakeSocket('') # Emulate server dropping connection
797+
conn.sock = FakeSocket('Invalid status line')
781798
conn.request('GET', '/')
782799
self.assertRaises(client.BadStatusLine, conn.getresponse)
783800
self.assertTrue(response.closed)
@@ -1174,6 +1191,78 @@ def testTimeoutAttribute(self):
11741191
httpConn.close()
11751192

11761193

1194+
class PersistenceTest(TestCase):
1195+
1196+
def test_reuse_reconnect(self):
1197+
# Should reuse or reconnect depending on header from server
1198+
tests = (
1199+
('1.0', '', False),
1200+
('1.0', 'Connection: keep-alive\r\n', True),
1201+
('1.1', '', True),
1202+
('1.1', 'Connection: close\r\n', False),
1203+
('1.0', 'Connection: keep-ALIVE\r\n', True),
1204+
('1.1', 'Connection: cloSE\r\n', False),
1205+
)
1206+
for version, header, reuse in tests:
1207+
with self.subTest(version=version, header=header):
1208+
msg = (
1209+
'HTTP/{} 200 OK\r\n'
1210+
'{}'
1211+
'Content-Length: 12\r\n'
1212+
'\r\n'
1213+
'Dummy body\r\n'
1214+
).format(version, header)
1215+
conn = FakeSocketHTTPConnection(msg)
1216+
self.assertIsNone(conn.sock)
1217+
conn.request('GET', '/open-connection')
1218+
with conn.getresponse() as response:
1219+
self.assertEqual(conn.sock is None, not reuse)
1220+
response.read()
1221+
self.assertEqual(conn.sock is None, not reuse)
1222+
self.assertEqual(conn.connections, 1)
1223+
conn.request('GET', '/subsequent-request')
1224+
self.assertEqual(conn.connections, 1 if reuse else 2)
1225+
1226+
def test_disconnected(self):
1227+
1228+
def make_reset_reader(text):
1229+
"""Return BufferedReader that raises ECONNRESET at EOF"""
1230+
stream = io.BytesIO(text)
1231+
def readinto(buffer):
1232+
size = io.BytesIO.readinto(stream, buffer)
1233+
if size == 0:
1234+
raise ConnectionResetError()
1235+
return size
1236+
stream.readinto = readinto
1237+
return io.BufferedReader(stream)
1238+
1239+
tests = (
1240+
(io.BytesIO, client.RemoteDisconnected),
1241+
(make_reset_reader, ConnectionResetError),
1242+
)
1243+
for stream_factory, exception in tests:
1244+
with self.subTest(exception=exception):
1245+
conn = FakeSocketHTTPConnection(b'', stream_factory)
1246+
conn.request('GET', '/eof-response')
1247+
self.assertRaises(exception, conn.getresponse)
1248+
self.assertIsNone(conn.sock)
1249+
# HTTPConnection.connect() should be automatically invoked
1250+
conn.request('GET', '/reconnect')
1251+
self.assertEqual(conn.connections, 2)
1252+
1253+
def test_100_close(self):
1254+
conn = FakeSocketHTTPConnection(
1255+
b'HTTP/1.1 100 Continue\r\n'
1256+
b'\r\n'
1257+
# Missing final response
1258+
)
1259+
conn.request('GET', '/', headers={'Expect': '100-continue'})
1260+
self.assertRaises(client.RemoteDisconnected, conn.getresponse)
1261+
self.assertIsNone(conn.sock)
1262+
conn.request('GET', '/reconnect')
1263+
self.assertEqual(conn.connections, 2)
1264+
1265+
11771266
class HTTPSTest(TestCase):
11781267

11791268
def setUp(self):
@@ -1513,6 +1602,7 @@ def test_tunnel_debuglog(self):
15131602
@support.reap_threads
15141603
def test_main(verbose=None):
15151604
support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest,
1605+
PersistenceTest,
15161606
HTTPSTest, RequestBodyTest, SourceAddressTest,
15171607
HTTPResponseTest, ExtendedReadTest,
15181608
ExtendedReadTestChunked, TunnelTests)

Lib/xmlrpc/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,7 @@ def request(self, host, handler, request_body, verbose=False):
11281128
if i or e.errno not in (errno.ECONNRESET, errno.ECONNABORTED,
11291129
errno.EPIPE):
11301130
raise
1131-
except http.client.BadStatusLine: #close after we sent request
1131+
except http.client.RemoteDisconnected:
11321132
if i:
11331133
raise
11341134

0 commit comments

Comments
 (0)