From 9b7946560b1bb30e86bf404de5a0a498ceed75fa Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Sat, 28 Feb 2026 19:49:04 +0200 Subject: [PATCH] [3.13] gh-142352: Fix `asyncio` `start_tls()` to transfer buffered data from StreamReader (GH-142354) (cherry picked from commit 0598f4a8999b96409e0a2bf9c480afc76a876860) Co-authored-by: Maksym Kasimov <39828623+kasimov-maxim@users.noreply.github.com> Co-authored-by: Kumar Aditya --- Lib/asyncio/base_events.py | 11 +++++ Lib/test/test_asyncio/test_streams.py | 42 +++++++++++++++++++ ...2-06-16-14-18.gh-issue-142352.pW5HLX88.rst | 4 ++ 3 files changed, 57 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-12-06-16-14-18.gh-issue-142352.pW5HLX88.rst diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 356fc3d7913ed3..84cad10636feef 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1348,6 +1348,17 @@ async def start_tls(self, transport, protocol, sslcontext, *, # have a chance to get called before "ssl_protocol.connection_made()". transport.pause_reading() + # gh-142352: move buffered StreamReader data to SSLProtocol + if server_side: + from .streams import StreamReaderProtocol + if isinstance(protocol, StreamReaderProtocol): + stream_reader = getattr(protocol, '_stream_reader', None) + if stream_reader is not None: + buffer = stream_reader._buffer + if buffer: + ssl_protocol._incoming.write(buffer) + buffer.clear() + transport.set_protocol(ssl_protocol) conmade_cb = self.call_soon(ssl_protocol.connection_made, transport) resume_cb = self.call_soon(transport.resume_reading) diff --git a/Lib/test/test_asyncio/test_streams.py b/Lib/test/test_asyncio/test_streams.py index 3040ca55fae0e9..f688c12649e543 100644 --- a/Lib/test/test_asyncio/test_streams.py +++ b/Lib/test/test_asyncio/test_streams.py @@ -868,6 +868,48 @@ def test_read_all_from_pipe_reader(self): data = self.loop.run_until_complete(reader.read(-1)) self.assertEqual(data, b'data') + @unittest.skipIf(ssl is None, 'No ssl module') + def test_start_tls_buffered_data(self): + # gh-142352: test start_tls() with buffered data + + async def server_handler(client_reader, client_writer): + # Wait for TLS ClientHello to be buffered before start_tls(). + await client_reader._wait_for_data('test_start_tls_buffered_data'), + self.assertTrue(client_reader._buffer) + await client_writer.start_tls(test_utils.simple_server_sslcontext()) + + line = await client_reader.readline() + self.assertEqual(line, b"ping\n") + client_writer.write(b"pong\n") + await client_writer.drain() + client_writer.close() + await client_writer.wait_closed() + + async def client(addr): + reader, writer = await asyncio.open_connection(*addr) + await writer.start_tls(test_utils.simple_client_sslcontext()) + + writer.write(b"ping\n") + await writer.drain() + line = await reader.readline() + self.assertEqual(line, b"pong\n") + writer.close() + await writer.wait_closed() + + async def run_test(): + server = await asyncio.start_server( + server_handler, socket_helper.HOSTv4, 0) + server_addr = server.sockets[0].getsockname() + + await client(server_addr) + server.close() + await server.wait_closed() + + messages = [] + self.loop.set_exception_handler(lambda loop, ctx: messages.append(ctx)) + self.loop.run_until_complete(run_test()) + self.assertEqual(messages, []) + def test_streamreader_constructor_without_loop(self): with self.assertRaisesRegex(RuntimeError, 'no current event loop'): asyncio.StreamReader() diff --git a/Misc/NEWS.d/next/Library/2025-12-06-16-14-18.gh-issue-142352.pW5HLX88.rst b/Misc/NEWS.d/next/Library/2025-12-06-16-14-18.gh-issue-142352.pW5HLX88.rst new file mode 100644 index 00000000000000..13e38b118175b4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-06-16-14-18.gh-issue-142352.pW5HLX88.rst @@ -0,0 +1,4 @@ +Fix :meth:`asyncio.StreamWriter.start_tls` to transfer buffered data from +:class:`~asyncio.StreamReader` to the SSL layer, preventing data loss when +upgrading a connection to TLS mid-stream (e.g., when implementing PROXY +protocol support).