]> granicus.if.org Git - python/commitdiff
Issue #16723: httplib.HTTPResponse no longer marked closed when the connection
authorSerhiy Storchaka <storchaka@gmail.com>
Wed, 6 Feb 2013 08:35:40 +0000 (10:35 +0200)
committerSerhiy Storchaka <storchaka@gmail.com>
Wed, 6 Feb 2013 08:35:40 +0000 (10:35 +0200)
is automatically closed.

1  2 
Lib/http/client.py
Lib/test/test_httplib.py
Misc/NEWS

index 62d9cff889f3b3c95e17e549318268747918fecb,5466d0618d33c5c32bdcf09992a7e11594fcea50..4663d439e32190d3527772f24ba3d3ed03f2fa0a
@@@ -490,157 -486,103 +494,157 @@@ class HTTPResponse(io.RawIOBase)
              return b""
  
          if self._method == "HEAD":
-             self.close()
+             self._close_conn()
              return b""
  
 -        if self.chunked:
 -            return self._read_chunked(amt)
 +        if amt is not None:
 +            # Amount is given, so call base class version
 +            # (which is implemented in terms of self.readinto)
 +            return super(HTTPResponse, self).read(amt)
 +        else:
 +            # Amount is not given (unbounded read) so we must check self.length
 +            # and self.chunked
 +
 +            if self.chunked:
 +                return self._readall_chunked()
  
 -        if amt is None:
 -            # unbounded read
              if self.length is None:
                  s = self.fp.read()
              else:
                  try:
                      s = self._safe_read(self.length)
                  except IncompleteRead:
-                     self.close()
+                     self._close_conn()
                      raise
                  self.length = 0
-             self.close()        # we read everything
+             self._close_conn()        # we read everything
              return s
  
-             self.close()
 +    def readinto(self, b):
 +        if self.fp is None:
 +            return 0
 +
 +        if self._method == "HEAD":
++            self._close_conn()
 +            return 0
 +
 +        if self.chunked:
 +            return self._readinto_chunked(b)
 +
          if self.length is not None:
 -            if amt > self.length:
 +            if len(b) > self.length:
                  # clip the read to the "end of response"
 -                amt = self.length
 +                b = memoryview(b)[0:self.length]
  
          # we do not use _safe_read() here because this may be a .will_close
          # connection, and the user is reading more bytes than will be provided
          # (for example, reading in 1k chunks)
 -        s = self.fp.read(amt)
 -        if not s:
 +        n = self.fp.readinto(b)
 +        if not n:
              # Ideally, we would raise IncompleteRead if the content-length
              # wasn't satisfied, but it might break compatibility.
-             self.close()
+             self._close_conn()
          elif self.length is not None:
 -            self.length -= len(s)
 +            self.length -= n
              if not self.length:
-                 self.close()
+                 self._close_conn()
 +        return n
  
 -        return s
 +    def _read_next_chunk_size(self):
 +        # Read the next chunk size from the file
 +        line = self.fp.readline(_MAXLINE + 1)
 +        if len(line) > _MAXLINE:
 +            raise LineTooLong("chunk size")
 +        i = line.find(b";")
 +        if i >= 0:
 +            line = line[:i] # strip chunk-extensions
 +        try:
 +            return int(line, 16)
 +        except ValueError:
 +            # close the connection as protocol synchronisation is
 +            # probably lost
-             self.close()
++            self._close_conn()
 +            raise
  
 -    def _read_chunked(self, amt):
 +    def _read_and_discard_trailer(self):
 +        # read and discard trailer up to the CRLF terminator
 +        ### note: we shouldn't have any trailers!
 +        while True:
 +            line = self.fp.readline(_MAXLINE + 1)
 +            if len(line) > _MAXLINE:
 +                raise LineTooLong("trailer line")
 +            if not line:
 +                # a vanishingly small number of sites EOF without
 +                # sending the trailer
 +                break
 +            if line in (b'\r\n', b'\n', b''):
 +                break
 +
 +    def _readall_chunked(self):
          assert self.chunked != _UNKNOWN
          chunk_left = self.chunk_left
          value = []
          while True:
              if chunk_left is None:
 -                line = self.fp.readline(_MAXLINE + 1)
 -                if len(line) > _MAXLINE:
 -                    raise LineTooLong("chunk size")
 -                i = line.find(b";")
 -                if i >= 0:
 -                    line = line[:i] # strip chunk-extensions
                  try:
 -                    chunk_left = int(line, 16)
 +                    chunk_left = self._read_next_chunk_size()
 +                    if chunk_left == 0:
 +                        break
                  except ValueError:
 -                    # close the connection as protocol synchronisation is
 -                    # probably lost
 -                    self._close_conn()
                      raise IncompleteRead(b''.join(value))
 -                if chunk_left == 0:
 -                    break
 -            if amt is None:
 -                value.append(self._safe_read(chunk_left))
 -            elif amt < chunk_left:
 -                value.append(self._safe_read(amt))
 -                self.chunk_left = chunk_left - amt
 -                return b''.join(value)
 -            elif amt == chunk_left:
 -                value.append(self._safe_read(amt))
 +            value.append(self._safe_read(chunk_left))
 +
 +            # we read the whole chunk, get another
 +            self._safe_read(2)      # toss the CRLF at the end of the chunk
 +            chunk_left = None
 +
 +        self._read_and_discard_trailer()
 +
 +        # we read everything; close the "file"
-         self.close()
++        self._close_conn()
 +
 +        return b''.join(value)
 +
 +    def _readinto_chunked(self, b):
 +        assert self.chunked != _UNKNOWN
 +        chunk_left = self.chunk_left
 +
 +        total_bytes = 0
 +        mvb = memoryview(b)
 +        while True:
 +            if chunk_left is None:
 +                try:
 +                    chunk_left = self._read_next_chunk_size()
 +                    if chunk_left == 0:
 +                        break
 +                except ValueError:
 +                    raise IncompleteRead(bytes(b[0:total_bytes]))
 +
 +            if len(mvb) < chunk_left:
 +                n = self._safe_readinto(mvb)
 +                self.chunk_left = chunk_left - n
 +                return total_bytes + n
 +            elif len(mvb) == chunk_left:
 +                n = self._safe_readinto(mvb)
                  self._safe_read(2)  # toss the CRLF at the end of the chunk
                  self.chunk_left = None
 -                return b''.join(value)
 +                return total_bytes + n
              else:
 -                value.append(self._safe_read(chunk_left))
 -                amt -= chunk_left
 +                temp_mvb = mvb[0:chunk_left]
 +                n = self._safe_readinto(temp_mvb)
 +                mvb = mvb[n:]
 +                total_bytes += n
  
              # we read the whole chunk, get another
              self._safe_read(2)      # toss the CRLF at the end of the chunk
              chunk_left = None
  
 -        # read and discard trailer up to the CRLF terminator
 -        ### note: we shouldn't have any trailers!
 -        while True:
 -            line = self.fp.readline(_MAXLINE + 1)
 -            if len(line) > _MAXLINE:
 -                raise LineTooLong("trailer line")
 -            if not line:
 -                # a vanishingly small number of sites EOF without
 -                # sending the trailer
 -                break
 -            if line in (b'\r\n', b'\n', b''):
 -                break
 +        self._read_and_discard_trailer()
  
          # we read everything; close the "file"
-         self.close()
+         self._close_conn()
  
 -        return b''.join(value)
 +        return total_bytes
  
      def _safe_read(self, amt):
          """Read the number of bytes requested, compensating for partial reads.
index a58a59247645efd383d0ad09c6bc0b4125294301,420302c332c7011ba85c3d0606d20fcad6089492..db123dcb56f7b4a4dd83f47a7735d48a9b51d0e1
@@@ -185,24 -188,10 +188,30 @@@ class BasicTest(TestCase)
          self.assertFalse(resp.isclosed())
          self.assertEqual(resp.read(2), b'xt')
          self.assertTrue(resp.isclosed())
+         self.assertFalse(resp.closed)
+         resp.close()
+         self.assertTrue(resp.closed)
  
 +    def test_partial_readintos(self):
 +        # if we have a length, the system knows when to close itself
 +        # same behaviour than when we read the whole thing with read()
 +        body = "HTTP/1.1 200 Ok\r\nContent-Length: 4\r\n\r\nText"
 +        sock = FakeSocket(body)
 +        resp = client.HTTPResponse(sock)
 +        resp.begin()
 +        b = bytearray(2)
 +        n = resp.readinto(b)
 +        self.assertEqual(n, 2)
 +        self.assertEqual(bytes(b), b'Te')
 +        self.assertFalse(resp.isclosed())
 +        n = resp.readinto(b)
 +        self.assertEqual(n, 2)
 +        self.assertEqual(bytes(b), b'xt')
 +        self.assertTrue(resp.isclosed())
++        self.assertFalse(resp.closed)
++        resp.close()
++        self.assertTrue(resp.closed)
 +
      def test_partial_reads_no_content_length(self):
          # when no length is present, the socket should be gracefully closed when
          # all data was read
          self.assertEqual(resp.read(2), b'xt')
          self.assertEqual(resp.read(1), b'')
          self.assertTrue(resp.isclosed())
+         self.assertFalse(resp.closed)
+         resp.close()
+         self.assertTrue(resp.closed)
  
 +    def test_partial_readintos_no_content_length(self):
 +        # when no length is present, the socket should be gracefully closed when
 +        # all data was read
 +        body = "HTTP/1.1 200 Ok\r\n\r\nText"
 +        sock = FakeSocket(body)
 +        resp = client.HTTPResponse(sock)
 +        resp.begin()
 +        b = bytearray(2)
 +        n = resp.readinto(b)
 +        self.assertEqual(n, 2)
 +        self.assertEqual(bytes(b), b'Te')
 +        self.assertFalse(resp.isclosed())
 +        n = resp.readinto(b)
 +        self.assertEqual(n, 2)
 +        self.assertEqual(bytes(b), b'xt')
 +        n = resp.readinto(b)
 +        self.assertEqual(n, 0)
 +        self.assertTrue(resp.isclosed())
 +
      def test_partial_reads_incomplete_body(self):
          # if the server shuts down the connection before the whole
          # content-length is delivered, the socket is gracefully closed
          self.assertEqual(resp.read(2), b'xt')
          self.assertEqual(resp.read(1), b'')
          self.assertTrue(resp.isclosed())
 +
 +    def test_partial_readintos_incomplete_body(self):
 +        # if the server shuts down the connection before the whole
 +        # content-length is delivered, the socket is gracefully closed
 +        body = "HTTP/1.1 200 Ok\r\nContent-Length: 10\r\n\r\nText"
 +        sock = FakeSocket(body)
 +        resp = client.HTTPResponse(sock)
 +        resp.begin()
 +        b = bytearray(2)
 +        n = resp.readinto(b)
 +        self.assertEqual(n, 2)
 +        self.assertEqual(bytes(b), b'Te')
 +        self.assertFalse(resp.isclosed())
 +        n = resp.readinto(b)
 +        self.assertEqual(n, 2)
 +        self.assertEqual(bytes(b), b'xt')
 +        n = resp.readinto(b)
 +        self.assertEqual(n, 0)
 +        self.assertTrue(resp.isclosed())
+         self.assertFalse(resp.closed)
+         resp.close()
+         self.assertTrue(resp.closed)
  
      def test_host_port(self):
          # Check invalid host_port
          self.assertEqual(resp.status, 200)
          self.assertEqual(resp.reason, 'OK')
          self.assertTrue(resp.isclosed())
+         self.assertFalse(resp.closed)
+         resp.close()
+         self.assertTrue(resp.closed)
  
 +    def test_readinto_chunked_head(self):
 +        chunked_start = (
 +            'HTTP/1.1 200 OK\r\n'
 +            'Transfer-Encoding: chunked\r\n\r\n'
 +            'a\r\n'
 +            'hello world\r\n'
 +            '1\r\n'
 +            'd\r\n'
 +        )
 +        sock = FakeSocket(chunked_start + '0\r\n')
 +        resp = client.HTTPResponse(sock, method="HEAD")
 +        resp.begin()
 +        b = bytearray(5)
 +        n = resp.readinto(b)
 +        self.assertEqual(n, 0)
 +        self.assertEqual(bytes(b), b'\x00'*5)
 +        self.assertEqual(resp.status, 200)
 +        self.assertEqual(resp.reason, 'OK')
 +        self.assertTrue(resp.isclosed())
++        self.assertFalse(resp.closed)
++        resp.close()
++        self.assertTrue(resp.closed)
 +
      def test_negative_content_length(self):
          sock = FakeSocket(
              'HTTP/1.1 200 OK\r\nContent-Length: -1\r\n\r\nHello\r\n')
diff --cc Misc/NEWS
Simple merge