diff --git a/Lib/http/client.py b/Lib/http/client.py index 0cce49cadc09fa..25f95418b2127d 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -668,6 +668,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: @@ -694,10 +696,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): diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 47e3914d1dd62e..b83c7725426b5a 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -1686,6 +1686,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): """ diff --git a/Misc/NEWS.d/next/Library/2024-02-27-20-56-44.gh-issue-115997.LqLP4V.rst b/Misc/NEWS.d/next/Library/2024-02-27-20-56-44.gh-issue-115997.LqLP4V.rst new file mode 100644 index 00000000000000..9fd6e790ce4af9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-02-27-20-56-44.gh-issue-115997.LqLP4V.rst @@ -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.