Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1641,6 +1641,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()
# Truncate the content to the last newline.
content = content[:content.rindex(b"\n") - 1]
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.split(b"\n")[-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