Skip to content

gh-115997: Make HTTPResponse.read1 and readline raise IncompleteRead #115998

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
7 changes: 7 additions & 0 deletions Lib/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,8 @@ def read1(self, n=-1):
result = self.fp.read1(n)
if not result and n:
self._close_conn()
if self.length:
raise IncompleteRead(result)
elif self.length is not None:
self.length -= len(result)
if not self.length:
Expand All @@ -691,10 +693,15 @@ def readline(self, limit=-1):
result = self.fp.readline(limit)
if not result and limit:
self._close_conn()
if self.length:
raise IncompleteRead(result)
elif self.length is not None:
self.length -= len(result)
if not self.length:
self._close_conn()
elif len(result) != limit and not result.endswith(b"\n"):
self._close_conn()
raise IncompleteRead(result)
return result

def _read1_chunked(self, n):
Expand Down
68 changes: 68 additions & 0 deletions Lib/test/test_httplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1640,6 +1640,74 @@ class ExtendedReadTestContentLengthKnown(ExtendedReadTest):
_header, _body = ExtendedReadTest.lines.split('\r\n\r\n', 1)
lines = _header + f'\r\nContent-Length: {len(_body)}\r\n\r\n' + _body

def _test_incomplete_read(self, read_meth, expected_none):
resp = self.resp
# Reduce the size of content the response object will read to
# cause the incomplete read.
resp.fp.read(1)
with self.assertRaises(client.IncompleteRead) as cm:
while True:
data = read_meth()
if not data:
break
exception = cm.exception
# Unlike `read1` and `readline`, `read` tries to read the whole
# content during one call, so it's partial is not empty in this
# case.
# `read1` and `readline` raise `IncompleteRead` only when they
# read 0 bytes before all expected content has been read, so the
# partial is empty.
if read_meth == self.resp.read:
expected_partial = self._body[1:].encode()
else:
expected_partial = b""
self.assertEqual(exception.partial, expected_partial)
if expected_none:
self.assertIsNone(exception.expected)
else:
self.assertEqual(exception.expected, 1)
self.assertTrue(resp.isclosed())

def test_read_incomplete_read(self):
self._test_incomplete_read(self.resp.read, expected_none=False)

def test_read1_incomplete_read(self):
self._test_incomplete_read(self.resp.read1, expected_none=True)

def test_readline_incomplete_read_with_complete_line(self):
"""
Test that IncompleteRead is raised when readline finishes
reading a response but the needed content length is not reached.
"""
resp = self.resp
content = resp.fp.read()
# For this test case, we must ensure that the last byte read
# will be a newline. There is a different handling of readline
# not reaching a newline.
content = content[:-1] + b"\n"
resp.fp = io.BytesIO(content)
self._test_incomplete_read(self.resp.readline, expected_none=True)

def test_readline_incomplete_read_with_incomplete_line(self):
"""
Test that IncompleteRead is raised when readline is expected
to read a line fully but a newline is not reached.
"""
resp = self.resp
content = resp.fp.read()
# Remove last newline and following bytes.
content = content.rsplit(b'\n', 1)[0]
resp.fp = io.BytesIO(content)
with self.assertRaises(client.IncompleteRead) as cm:
while True:
data = resp.readline()
if not data:
break
exception = cm.exception
self.assertEqual(exception.partial, content.rsplit(b'\n', 1)[1])
self.assertIsNone(exception.expected)
self.assertTrue(resp.isclosed())


class ExtendedReadTestChunked(ExtendedReadTest):
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Make ``http.client.HTTPResponse.read1`` and
``http.client.HTTPResponse.readline`` raise :exc:`http.client.IncompleteRead`
instead of returning zero bytes if a connection is closed before an expected
number of bytes has been read. Also, ``http.client.HTTPResponse.readline``
raises when an expected newline has not been reached. Patch by Illia Volochii.
Loading