| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 142bf56 commit cae7bdb
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -175,6 +175,17 @@ The following exceptions are raised as appropriate: | |||
| 175 | 175 | is received in the HTTP protocol from the server. | |
| 176 | 176 | ||
| 177 | 177 | ||
| 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 | + | ||
| 178 | 189 | The constants defined in this module are: | |
| 179 | 190 | ||
| 180 | 191 | .. data:: HTTP_PORT | |
@@ -247,6 +258,11 @@ HTTPConnection Objects | |||
| 247 | 258 | Note that you must have read the whole response before you can send a new | |
| 248 | 259 | request to the server. | |
| 249 | 260 | ||
| 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 | + | ||
| 250 | 266 | ||
| 251 | 267 | .. method:: HTTPConnection.set_debuglevel(level) | |
| 252 | 268 | ||
@@ -285,7 +301,9 @@ HTTPConnection Objects | |||
| 285 | 301 | ||
| 286 | 302 | .. method:: HTTPConnection.connect() | |
| 287 | 303 | ||
| 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. | ||
| 289 | 307 | ||
| 290 | 308 | ||
| 291 | 309 | .. method:: HTTPConnection.close() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -20,10 +20,12 @@ | |||
| 20 | 20 | | ( putheader() )* endheaders() | |
| 21 | 21 | v | |
| 22 | 22 | 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] | ||
| 27 | 29 | |\____________________ | |
| 28 | 30 | | | | |
| 29 | 31 | | response.read() | putrequest() | |
@@ -83,7 +85,8 @@ | |||
| 83 | 85 | "UnknownTransferEncoding", "UnimplementedFileMode", | |
| 84 | 86 | "IncompleteRead", "InvalidURL", "ImproperConnectionState", | |
| 85 | 87 | "CannotSendRequest", "CannotSendHeader", "ResponseNotReady", | |
| 86 | - "BadStatusLine", "LineTooLong", "error", "responses"] | ||
| 88 | + "BadStatusLine", "LineTooLong", "RemoteDisconnected", "error", | ||
| 89 | + "responses"] | ||
| 87 | 90 | ||
| 88 | 91 | HTTP_PORT = 80 | |
| 89 | 92 | HTTPS_PORT = 443 | |
@@ -245,7 +248,8 @@ def _read_status(self): | |||
| 245 | 248 | if not line: | |
| 246 | 249 | # Presumably, the server closed the connection before | |
| 247 | 250 | # sending a valid response. | |
| 248 | - raise BadStatusLine(line) | ||
| 251 | + raise RemoteDisconnected("Remote end closed connection without" | ||
| 252 | + " response") | ||
| 249 | 253 | try: | |
| 250 | 254 | version, status, reason = line.split(None, 2) | |
| 251 | 255 | except ValueError: | |
@@ -1160,7 +1164,11 @@ class the response_class variable. | |||
| 1160 | 1164 | response = self.response_class(self.sock, method=self._method) | |
| 1161 | 1165 | ||
| 1162 | 1166 | try: | |
| 1163 | - response.begin() | ||
| 1167 | + try: | ||
| 1168 | + response.begin() | ||
| 1169 | + except ConnectionError: | ||
| 1170 | + self.close() | ||
| 1171 | + raise | ||
| 1164 | 1172 | assert response.will_close != _UNKNOWN | |
| 1165 | 1173 | self.__state = _CS_IDLE | |
| 1166 | 1174 | ||
@@ -1292,5 +1300,10 @@ def __init__(self, line_type): | |||
| 1292 | 1300 | HTTPException.__init__(self, "got more than %d bytes when reading %s" | |
| 1293 | 1301 | % (_MAXLINE, line_type)) | |
| 1294 | 1302 | ||
| 1303 | + class RemoteDisconnected(ConnectionResetError, BadStatusLine): | ||
| 1304 | + def __init__(self, *pos, **kw): | ||
| 1305 | + BadStatusLine.__init__(self, "") | ||
| 1306 | + ConnectionResetError.__init__(self, *pos, **kw) | ||
| 1307 | + | ||
| 1295 | 1308 | # for backwards compatibility | |
| 1296 | 1309 | error = HTTPException | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -107,6 +107,23 @@ def readline(self, length=None): | |||
| 107 | 107 | raise AssertionError('caller tried to read past EOF') | |
| 108 | 108 | return data | |
| 109 | 109 | ||
| 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 | + | ||
| 110 | 127 | class HeaderTests(TestCase): | |
| 111 | 128 | def test_auto_headers(self): | |
| 112 | 129 | # Some headers are added automatically, but should not be added by | |
@@ -777,7 +794,7 @@ def __init__(self, *pos, **kw): | |||
| 777 | 794 | response = self # Avoid garbage collector closing the socket | |
| 778 | 795 | client.HTTPResponse.__init__(self, *pos, **kw) | |
| 779 | 796 | conn.response_class = Response | |
| 780 | - conn.sock = FakeSocket('') # Emulate server dropping connection | ||
| 797 | + conn.sock = FakeSocket('Invalid status line') | ||
| 781 | 798 | conn.request('GET', '/') | |
| 782 | 799 | self.assertRaises(client.BadStatusLine, conn.getresponse) | |
| 783 | 800 | self.assertTrue(response.closed) | |
@@ -1174,6 +1191,78 @@ def testTimeoutAttribute(self): | |||
| 1174 | 1191 | httpConn.close() | |
| 1175 | 1192 | ||
| 1176 | 1193 | ||
| 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 | + | ||
| 1177 | 1266 | class HTTPSTest(TestCase): | |
| 1178 | 1267 | ||
| 1179 | 1268 | def setUp(self): | |
@@ -1513,6 +1602,7 @@ def test_tunnel_debuglog(self): | |||
| 1513 | 1602 | @support.reap_threads | |
| 1514 | 1603 | def test_main(verbose=None): | |
| 1515 | 1604 | support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest, | |
| 1605 | + PersistenceTest, | ||
| 1516 | 1606 | HTTPSTest, RequestBodyTest, SourceAddressTest, | |
| 1517 | 1607 | HTTPResponseTest, ExtendedReadTest, | |
| 1518 | 1608 | ExtendedReadTestChunked, TunnelTests) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1128,7 +1128,7 @@ def request(self, host, handler, request_body, verbose=False): | |||
| 1128 | 1128 | if i or e.errno not in (errno.ECONNRESET, errno.ECONNABORTED, | |
| 1129 | 1129 | errno.EPIPE): | |
| 1130 | 1130 | raise | |
| 1131 | - except http.client.BadStatusLine: #close after we sent request | ||
| 1131 | + except http.client.RemoteDisconnected: | ||
| 1132 | 1132 | if i: | |
| 1133 | 1133 | raise | |
| 1134 | 1134 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments