Issue #19524: Fixed resource leak in the HTTP connection when an invalid
authorSerhiy Storchaka <storchaka@gmail.com>
Sat, 6 Sep 2014 18:41:39 +0000 (21:41 +0300)
committerSerhiy Storchaka <storchaka@gmail.com>
Sat, 6 Sep 2014 18:41:39 +0000 (21:41 +0300)
response is received.  Patch by Martin Panter.

Lib/test/test_urllib.py
Lib/test/test_urllib2.py
Lib/urllib/request.py
Misc/ACKS
Misc/NEWS

index 1a5013ed2696a5b507169aed5bae3c1d89211ca7..7eb34c8ccfa192a5db977527e8deb209a3bda2cc 100644 (file)
@@ -48,43 +48,48 @@ def urlopen(url, data=None, proxies=None):
         return opener.open(url, data)
 
 
-class FakeHTTPMixin(object):
-    def fakehttp(self, fakedata):
-        class FakeSocket(io.BytesIO):
-            io_refs = 1
+def fakehttp(fakedata):
+    class FakeSocket(io.BytesIO):
+        io_refs = 1
 
-            def sendall(self, data):
-                FakeHTTPConnection.buf = data
+        def sendall(self, data):
+            FakeHTTPConnection.buf = data
 
-            def makefile(self, *args, **kwds):
-                self.io_refs += 1
-                return self
+        def makefile(self, *args, **kwds):
+            self.io_refs += 1
+            return self
 
-            def read(self, amt=None):
-                if self.closed:
-                    return b""
-                return io.BytesIO.read(self, amt)
+        def read(self, amt=None):
+            if self.closed:
+                return b""
+            return io.BytesIO.read(self, amt)
 
-            def readline(self, length=None):
-                if self.closed:
-                    return b""
-                return io.BytesIO.readline(self, length)
+        def readline(self, length=None):
+            if self.closed:
+                return b""
+            return io.BytesIO.readline(self, length)
 
-            def close(self):
-                self.io_refs -= 1
-                if self.io_refs == 0:
-                    io.BytesIO.close(self)
+        def close(self):
+            self.io_refs -= 1
+            if self.io_refs == 0:
+                io.BytesIO.close(self)
+
+    class FakeHTTPConnection(http.client.HTTPConnection):
 
-        class FakeHTTPConnection(http.client.HTTPConnection):
+        # buffer to store data for verification in urlopen tests.
+        buf = None
+        fakesock = FakeSocket(fakedata)
 
-            # buffer to store data for verification in urlopen tests.
-            buf = None
+        def connect(self):
+            self.sock = self.fakesock
 
-            def connect(self):
-                self.sock = FakeSocket(fakedata)
+    return FakeHTTPConnection
 
+
+class FakeHTTPMixin(object):
+    def fakehttp(self, fakedata):
         self._connection_class = http.client.HTTPConnection
-        http.client.HTTPConnection = FakeHTTPConnection
+        http.client.HTTPConnection = fakehttp(fakedata)
 
     def unfakehttp(self):
         http.client.HTTPConnection = self._connection_class
index 76208768a5bf06ad38dd53b6461bff3143feed39..9ea39a49b2df19b298a8a43c55b34b846c14b189 100644 (file)
@@ -1,5 +1,6 @@
 import unittest
 from test import support
+from test import test_urllib
 
 import os
 import io
@@ -13,6 +14,7 @@ import urllib.request
 from urllib.request import Request, OpenerDirector, _parse_proxy, _proxy_bypass_macosx_sysconf
 from urllib.parse import urlparse
 import urllib.error
+import http.client
 
 # XXX
 # Request
@@ -1393,6 +1395,33 @@ class HandlerTests(unittest.TestCase):
         self.assertEqual(len(http_handler.requests), 1)
         self.assertFalse(http_handler.requests[0].has_header(auth_header))
 
+    def test_http_closed(self):
+        """Test the connection is cleaned up when the response is closed"""
+        for (transfer, data) in (
+            ("Connection: close", b"data"),
+            ("Transfer-Encoding: chunked", b"4\r\ndata\r\n0\r\n\r\n"),
+            ("Content-Length: 4", b"data"),
+        ):
+            header = "HTTP/1.1 200 OK\r\n{}\r\n\r\n".format(transfer)
+            conn = test_urllib.fakehttp(header.encode() + data)
+            handler = urllib.request.AbstractHTTPHandler()
+            req = Request("http://dummy/")
+            req.timeout = None
+            with handler.do_open(conn, req) as resp:
+                resp.read()
+            self.assertTrue(conn.fakesock.closed,
+                "Connection not closed with {!r}".format(transfer))
+
+    def test_invalid_closed(self):
+        """Test the connection is cleaned up after an invalid response"""
+        conn = test_urllib.fakehttp(b"")
+        handler = urllib.request.AbstractHTTPHandler()
+        req = Request("http://dummy/")
+        req.timeout = None
+        with self.assertRaises(http.client.BadStatusLine):
+            handler.do_open(conn, req)
+        self.assertTrue(conn.fakesock.closed, "Connection not closed")
+
 
 class MiscTests(unittest.TestCase):
 
index a17c86859aa986248bb9f0211a6f1b6c3a8e43cb..67c7566c5b02413eddf359cfbd0ed0b954068fe7 100644 (file)
@@ -1170,18 +1170,21 @@ class AbstractHTTPHandler(BaseHandler):
             h.set_tunnel(req._tunnel_host, headers=tunnel_headers)
 
         try:
-            h.request(req.get_method(), req.selector, req.data, headers)
-        except OSError as err: # timeout error
-            h.close()
-            raise URLError(err)
-        else:
+            try:
+                h.request(req.get_method(), req.selector, req.data, headers)
+            except OSError as err: # timeout error
+                raise URLError(err)
             r = h.getresponse()
-            # If the server does not send us a 'Connection: close' header,
-            # HTTPConnection assumes the socket should be left open. Manually
-            # mark the socket to be closed when this response object goes away.
-            if h.sock:
-                h.sock.close()
-                h.sock = None
+        except:
+            h.close()
+            raise
+
+        # If the server does not send us a 'Connection: close' header,
+        # HTTPConnection assumes the socket should be left open. Manually
+        # mark the socket to be closed when this response object goes away.
+        if h.sock:
+            h.sock.close()
+            h.sock = None
 
         r.url = req.get_full_url()
         # This line replaces the .msg attribute of the HTTPResponse
index 5a388ffa12f905f80e18644cad9830778ba86353..e582b64da5e41bb86a442972cb720e7020aa3f96 100644 (file)
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1003,6 +1003,7 @@ Mike Pall
 Todd R. Palmer
 Juan David Ibáñez Palomar
 Jan Palus
+Martin Panter
 Mathias Panzenböck
 M. Papillon
 Peter Parente
index 11435b972ac6684f646d5722a1bf70c81602d67d..edbd80b09469fbe35dac27b881711b9e34a0516d 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -32,6 +32,9 @@ Core and Builtins
 Library
 -------
 
+- Issue #19524: Fixed resource leak in the HTTP connection when an invalid
+  response is received.  Patch by Martin Panter.
+
 - Issue #22051: turtledemo no longer reloads examples to re-run them.
   Initialization of variables and gui setup should be done in main(),
   which is called each time a demo is run, but not on import.