From 048ee831c4afc50389b06eac73447b96efcfcf38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:15:31 +0200 Subject: [PATCH 1/3] fix default handling of HTTP/0.9 requests --- Lib/http/server.py | 38 +++++++++++-------- Lib/test/test_httpservers.py | 20 ++++++++++ ...5-10-02-17-40-10.gh-issue-70765.zVlLZn.rst | 5 +++ 3 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-10-02-17-40-10.gh-issue-70765.zVlLZn.rst diff --git a/Lib/http/server.py b/Lib/http/server.py index a2ffbe2e44df64..e49a08df2a6d89 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -302,6 +302,7 @@ def parse_request(self): error response has already been sent back. """ + is_http_0_9 = False self.command = None # set in case of error on the first line self.request_version = version = self.default_request_version self.close_connection = True @@ -359,6 +360,7 @@ def parse_request(self): HTTPStatus.BAD_REQUEST, "Bad HTTP/0.9 request type (%r)" % command) return False + is_http_0_9 = True self.command, self.path = command, path # gh-87389: The purpose of replacing '//' with '/' is to protect @@ -369,22 +371,26 @@ def parse_request(self): self.path = '/' + self.path.lstrip('/') # Reduce to a single / # Examine the headers and look for a Connection directive. - try: - self.headers = http.client.parse_headers(self.rfile, - _class=self.MessageClass) - except http.client.LineTooLong as err: - self.send_error( - HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, - "Line too long", - str(err)) - return False - except http.client.HTTPException as err: - self.send_error( - HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, - "Too many headers", - str(err) - ) - return False + # For HTTP/0.9, headers are not expected at all. + if is_http_0_9: + self.headers = {} + else: + try: + self.headers = http.client.parse_headers(self.rfile, + _class=self.MessageClass) + except http.client.LineTooLong as err: + self.send_error( + HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, + "Line too long", + str(err)) + return False + except http.client.HTTPException as err: + self.send_error( + HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, + "Too many headers", + str(err) + ) + return False conntype = self.headers.get('Connection', "") if conntype.lower() == 'close': diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index 2548a7c5f292f0..7c756e98dbe632 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -362,6 +362,26 @@ def test_head_via_send_error(self): self.assertEqual(b'', data) +class HTTP09ServerTestCase(BaseTestCase): + + class request_handler(NoLogRequestHandler, BaseHTTPRequestHandler): + """Request handler for HTTP/0.9 server.""" + + def do_GET(self): + self.wfile.write(f'OK: here is {self.path}\r\n'.encode()) + + def setUp(self): + super().setUp() + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock = self.enterContext(self.sock) + self.sock.connect((self.HOST, self.PORT)) + + def test_http_0_9_server(self): + self.sock.send(b'GET /index.html\r\n') + res = self.sock.recv(1024) + self.assertEqual(res, b"OK: here is /index.html\r\n") + + def certdata_file(*path): return os.path.join(os.path.dirname(__file__), "certdata", *path) diff --git a/Misc/NEWS.d/next/Library/2025-10-02-17-40-10.gh-issue-70765.zVlLZn.rst b/Misc/NEWS.d/next/Library/2025-10-02-17-40-10.gh-issue-70765.zVlLZn.rst new file mode 100644 index 00000000000000..e1a9bbe9afe4d1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-02-17-40-10.gh-issue-70765.zVlLZn.rst @@ -0,0 +1,5 @@ +:mod:`http.server`: fix default handling of HTTP/0.9 requests in +:class:`~http.server.BaseHTTPRequestHandler`. Previously, +:meth:`!BaseHTTPRequestHandler.parse_request`` incorrectly +waited for headers in the request although those are not +supported in HTTP/0.9. Patch by Bénédikt Tran. From 1f16dab75e68f93e60b66b2a817306d36850e643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 5 Oct 2025 11:46:56 +0200 Subject: [PATCH 2/3] reduce diff --- Lib/http/server.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/Lib/http/server.py b/Lib/http/server.py index e49a08df2a6d89..160d3eefc7cbdf 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -370,27 +370,28 @@ def parse_request(self): if self.path.startswith('//'): self.path = '/' + self.path.lstrip('/') # Reduce to a single / - # Examine the headers and look for a Connection directive. # For HTTP/0.9, headers are not expected at all. if is_http_0_9: self.headers = {} - else: - try: - self.headers = http.client.parse_headers(self.rfile, - _class=self.MessageClass) - except http.client.LineTooLong as err: - self.send_error( - HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, - "Line too long", - str(err)) - return False - except http.client.HTTPException as err: - self.send_error( - HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, - "Too many headers", - str(err) - ) - return False + return True + + # Examine the headers and look for a Connection directive. + try: + self.headers = http.client.parse_headers(self.rfile, + _class=self.MessageClass) + except http.client.LineTooLong as err: + self.send_error( + HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, + "Line too long", + str(err)) + return False + except http.client.HTTPException as err: + self.send_error( + HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, + "Too many headers", + str(err) + ) + return False conntype = self.headers.get('Connection', "") if conntype.lower() == 'close': From edf1a1019b9749d0805f8d5daf53d965f1e30001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 5 Oct 2025 12:00:48 +0200 Subject: [PATCH 3/3] increase test coverage --- Lib/test/test_httpservers.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index 7c756e98dbe632..85d3a346439f6a 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -376,11 +376,28 @@ def setUp(self): self.sock = self.enterContext(self.sock) self.sock.connect((self.HOST, self.PORT)) - def test_http_0_9_server(self): + def test_simple_get(self): self.sock.send(b'GET /index.html\r\n') res = self.sock.recv(1024) self.assertEqual(res, b"OK: here is /index.html\r\n") + def test_invalid_request(self): + self.sock.send(b'POST /index.html\r\n') + res = self.sock.recv(1024) + self.assertIn(b"Bad HTTP/0.9 request type ('POST')", res) + + def test_single_request(self): + self.sock.send(b'GET /foo.html\r\n') + res = self.sock.recv(1024) + self.assertEqual(res, b"OK: here is /foo.html\r\n") + + self.sock.send(b'GET /bar.html\r\n') + res = self.sock.recv(1024) + # The server will not parse more input as it closed the connection. + # Note that the socket connection itself is still opened since the + # client is responsible for also closing it on their side. + self.assertEqual(res, b'') + def certdata_file(*path): return os.path.join(os.path.dirname(__file__), "certdata", *path)