diff --git a/Lib/http/client.py b/Lib/http/client.py index fe67d0f7963f78..201123930da00f 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -351,12 +351,15 @@ def begin(self, *, _max_headers=None): # for _read_and_discard_trailer() to enforce. self._max_headers = _max_headers - # read until we get a non-100 response + # read until we get a final response, skipping 1xx (informational) + # responses such as "100 Continue" or "103 Early Hints". + # "101 Switching Protocols" is returned to the caller since it ends + # the HTTP/1.1 exchange on this connection. for _ in range(_MAXINTERIMRESPONSES): version, status, reason = self._read_status() - if status != CONTINUE: + if not 100 <= status < 200 or status == SWITCHING_PROTOCOLS: break - # skip the header from the 100 response + # skip the header from the 1xx response skipped_headers = _read_headers(self.fp, _max_headers) if self.debuglevel > 0: print("headers:", skipped_headers) diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 1e5ec0d4e43c27..0964a6b08fa47f 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -1479,6 +1479,46 @@ def test_multiple_interim_responses(self): self.assertEqual(resp.read(), b'hello') resp.close() + def test_informational_responses(self): + # gh-72756: every 1xx response other than "101 Switching Protocols" + # is an interim response and must be skipped, like "100 Continue". + for interim in ('102 Processing', '103 Early Hints', '199 Unknown', + '103 Early Hints\r\n\r\nHTTP/1.1 100 Continue'): + with self.subTest(interim=interim): + body = ( + f'HTTP/1.1 {interim}\r\n' + 'Link: ; rel=preload; as=style\r\n' + '\r\n' + 'HTTP/1.1 200 OK\r\n' + 'Content-Length: 5\r\n' + '\r\n' + 'hello' + ) + resp = client.HTTPResponse(FakeSocket(body), method="GET") + resp.begin() + self.assertEqual(resp.status, 200) + self.assertEqual(resp.reason, 'OK') + self.assertIsNone(resp.getheader('Link')) + self.assertEqual(resp.read(), b'hello') + resp.close() + + def test_switching_protocols_response(self): + # "101 Switching Protocols" ends the HTTP/1.1 exchange, so it is + # returned to the caller instead of being skipped. + body = ( + 'HTTP/1.1 101 Switching Protocols\r\n' + 'Upgrade: websocket\r\n' + 'Connection: Upgrade\r\n' + '\r\n' + 'not HTTP' + ) + resp = client.HTTPResponse(FakeSocket(body), method="GET") + resp.begin() + self.assertEqual(resp.status, 101) + self.assertEqual(resp.getheader('Upgrade'), 'websocket') + self.assertEqual(resp.read(), b'') + resp.close() + def test_overflowing_chunked_line(self): body = ( 'HTTP/1.1 200 OK\r\n' diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index c82084ce299d77..3c5e08a3821321 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -329,7 +329,7 @@ def test_send_error(self): allow_transfer_encoding_codes = (HTTPStatus.NOT_MODIFIED, HTTPStatus.RESET_CONTENT) for code in (HTTPStatus.NO_CONTENT, HTTPStatus.NOT_MODIFIED, - HTTPStatus.PROCESSING, HTTPStatus.RESET_CONTENT, + HTTPStatus.RESET_CONTENT, HTTPStatus.SWITCHING_PROTOCOLS): self.con.request('SEND_ERROR', '/{}'.format(code)) res = self.con.getresponse() @@ -342,6 +342,23 @@ def test_send_error(self): data = res.read() self.assertEqual(b'', data) + def test_send_error_informational(self): + # http.client skips 1xx (informational) responses other than + # "101 Switching Protocols", so read this one from the socket. + self.con.close() + with socket.create_connection((self.HOST, self.PORT)) as sock: + sock.sendall(b'SEND_ERROR /102 HTTP/1.1\r\n\r\n') + response = b''.join(iter(lambda: sock.recv(1024), b'')) + head, sep, data = response.partition(b'\r\n\r\n') + self.assertEqual(sep, b'\r\n\r\n') + status_line, *header_lines = head.split(b'\r\n') + self.assertStartsWith(status_line, b'HTTP/1.1 102 ') + names = [line.partition(b':')[0].lower() for line in header_lines] + self.assertNotIn(b'content-length', names) + self.assertNotIn(b'content-type', names) + self.assertNotIn(b'transfer-encoding', names) + self.assertEqual(b'', data) + def test_head_via_send_error(self): allow_transfer_encoding_codes = (HTTPStatus.NOT_MODIFIED, HTTPStatus.RESET_CONTENT) diff --git a/Misc/NEWS.d/next/Library/2026-11-01-11-58-06.gh-issue-72756.6qU4Wg.rst b/Misc/NEWS.d/next/Library/2026-11-01-11-58-06.gh-issue-72756.6qU4Wg.rst new file mode 100644 index 00000000000000..706ec59278a0d4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-01-11-58-06.gh-issue-72756.6qU4Wg.rst @@ -0,0 +1,3 @@ +Fix :mod:`http.client` returning a 1xx informational response such as +``103 Early Hints`` as the final response. All 1xx responses except +``101 Switching Protocols`` are now skipped, like ``100 Continue``.