changeset 15218:c81dce8a7bb6

httpclient: update to 07d8c356f4d1 of py-nonblocking-http This addresses a defect when the server closes the socket before finishing a response (if it crashes, for example) first spotted in Issue2951.
author Augie Fackler <durin42@gmail.com>
date Mon, 10 Oct 2011 17:57:40 -0500
parents 42d0d4f63bf0
children 9d58569a8b92
files mercurial/httpclient/__init__.py mercurial/httpclient/tests/simple_http_test.py mercurial/httpclient/tests/test_chunked_transfer.py
diffstat 3 files changed, 44 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/httpclient/__init__.py	Mon Oct 10 13:52:54 2011 +0200
+++ b/mercurial/httpclient/__init__.py	Mon Oct 10 17:57:40 2011 -0500
@@ -171,6 +171,14 @@
             logger.info('cl: %r body: %r', self._content_len, self._body)
         try:
             data = self.sock.recv(INCOMING_BUFFER_SIZE)
+            # If the socket was readable and no data was read, that
+            # means the socket was closed. If this isn't a
+            # _CLOSE_IS_END socket, then something is wrong if we're
+            # here (we shouldn't enter _select() if the response is
+            # complete), so abort.
+            if not data and self._content_len != _LEN_CLOSE_IS_END:
+                raise HTTPRemoteClosedError(
+                    'server appears to have closed the socket mid-response')
         except socket.sslerror, e:
             if e.args[0] != socket.SSL_ERROR_WANT_READ:
                 raise
@@ -693,6 +701,11 @@
 class HTTPProxyConnectFailedException(httplib.HTTPException):
     """Connecting to the HTTP proxy failed."""
 
+
 class HTTPStateError(httplib.HTTPException):
     """Invalid internal state encountered."""
+
+
+class HTTPRemoteClosedError(httplib.HTTPException):
+    """The server closed the remote socket in the middle of a response."""
 # no-check-code
--- a/mercurial/httpclient/tests/simple_http_test.py	Mon Oct 10 13:52:54 2011 +0200
+++ b/mercurial/httpclient/tests/simple_http_test.py	Mon Oct 10 17:57:40 2011 -0500
@@ -380,6 +380,21 @@
         con.request('GET', '/')
         self.assertEqual(2, len(sockets))
 
+    def test_server_closes_before_end_of_body(self):
+        con = http.HTTPConnection('1.2.3.4:80')
+        con._connect()
+        s = con.sock
+        s.data = ['HTTP/1.1 200 OK\r\n',
+                  'Server: BogusServer 1.0\r\n',
+                  'Connection: Keep-Alive\r\n',
+                  'Content-Length: 16',
+                  '\r\n\r\n',
+                  'You can '] # Note: this is shorter than content-length
+        s.close_on_empty = True
+        con.request('GET', '/')
+        r1 = con.getresponse()
+        self.assertRaises(http.HTTPRemoteClosedError, r1.read)
+
     def test_no_response_raises_response_not_ready(self):
         con = http.HTTPConnection('foo')
         self.assertRaises(http.httplib.ResponseNotReady, con.getresponse)
--- a/mercurial/httpclient/tests/test_chunked_transfer.py	Mon Oct 10 13:52:54 2011 +0200
+++ b/mercurial/httpclient/tests/test_chunked_transfer.py	Mon Oct 10 17:57:40 2011 -0500
@@ -134,4 +134,20 @@
         con.request('GET', '/')
         self.assertStringEqual('hi there\nthere\nthere\nthere\nthere\n',
                                con.getresponse().read())
+
+    def testChunkedDownloadEarlyHangup(self):
+        con = http.HTTPConnection('1.2.3.4:80')
+        con._connect()
+        sock = con.sock
+        broken = chunkedblock('hi'*20)[:-1]
+        sock.data = ['HTTP/1.1 200 OK\r\n',
+                     'Server: BogusServer 1.0\r\n',
+                     'transfer-encoding: chunked',
+                     '\r\n\r\n',
+                     broken,
+                     ]
+        sock.close_on_empty = True
+        con.request('GET', '/')
+        resp = con.getresponse()
+        self.assertRaises(http.HTTPRemoteClosedError, resp.read)
 # no-check-code