You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
start_tls() returns a closed transport instead of raising when the client disconnects before the handshake (for eager tasks) #784
When using an eager task factory, if a client closes the connection before the TLS handshake, the server's StreamWriter.start_tls returns normally with a closed transport, and get_extra_info('socket') is None.
With plain asyncio, or with uvloop without eager tasks, start_tls raises ConnectionResetError, so uvloop should presumably do the same.
Here's a reproduction script for (disclosure, AI-generated, but I've run it and it does demonstrate the issue). This is for Python 3.13+ as it requires PSK functions in the ssl module, but the issue also occurs on Python 3.12 using a certificate instead of a PSK, so it's not PSK-specific.
(note this reproducer also contains a workaround for #779, otherwise one can't actually enable eager tasks at all)
I reproduced this with the same output as yours (Python 3.14.9, uvloop 0.23.0, built from source). The underlying bug isn't really TLS-specific: uvloop ignores transport.pause_reading() when it's called from connection_made().
UVBaseTransport._call_connection_made (uvloop/handles/basetransport.pyx, ~L120-154) calls protocol.connection_made(self) first and only then calls self._start_reading(). During connection_made the stream isn't reading yet, so pause_reading() (handles/stream.pyx L717) sees not self._is_reading() and does nothing. Reading then starts anyway. You can see it without TLS:
With an eager task factory, the start_server callback runs inside connection_made, because StreamReaderProtocol.connection_made creates the task and it starts eagerly. start_tls() (loop.pyx L1618) calls pause_reading(), which is a no-op here, and swaps in the SSLProtocol. The transport then starts reading, sees EOF, and closes before SSLProtocol.connection_made runs. SSLProtocol.connection_lost(None) is reached in the UNWRAPPED state and resolves the waiter with None, so start_tls returns a dead transport.
Fix: start reading before calling connection_made. No read callback can fire until control returns to the loop, so the ordering stays the same, and a pause from connection_made now actually applies:
--- a/uvloop/handles/basetransport.pyx+++ b/uvloop/handles/basetransport.pyx@@ -138,9 +138,13 @@
self._protocol_connected = 1
+ # Start reading before connection_made() so a pause_reading()+ # issued from it (e.g. start_tls() in an eager task) is honoured.+ self._start_reading()+
try:
self._protocol.connection_made(self)
except BaseException:
+ if self._is_alive():+ self._stop_reading()
self._wakeup_waiter()
raise
@@ -150,7 +154,6 @@
return
- self._start_reading()
self._wakeup_waiter()
Tested: I rebuilt 0.23.0 with Cython (regenerated loop.c) and applied the patch. Your script now prints start_tls raised ConnectionResetError in all four configurations, and the protocol snippet above no longer receives data while paused. test_tcp, test_unix, test_pipes, test_udp, test_base, test_context and test_sockets: 316 passed / 28 skipped, the same as an unpatched build. basetransport.pyx is identical on master.
Activity
hamarshehmhmd commentedon Oct 31, 2026
I reproduced this with the same output as yours (Python 3.14.9, uvloop 0.23.0, built from source). The underlying bug isn't really TLS-specific: uvloop ignores
transport.pause_reading()when it's called fromconnection_made().UVBaseTransport._call_connection_made(uvloop/handles/basetransport.pyx, ~L120-154) callsprotocol.connection_made(self)first and only then callsself._start_reading(). Duringconnection_madethe stream isn't reading yet, sopause_reading()(handles/stream.pyxL717) seesnot self._is_reading()and does nothing. Reading then starts anyway. You can see it without TLS:With an eager task factory, the
start_servercallback runs insideconnection_made, becauseStreamReaderProtocol.connection_madecreates the task and it starts eagerly.start_tls()(loop.pyxL1618) callspause_reading(), which is a no-op here, and swaps in theSSLProtocol. The transport then starts reading, sees EOF, and closes beforeSSLProtocol.connection_maderuns.SSLProtocol.connection_lost(None)is reached in the UNWRAPPED state and resolves the waiter withNone, sostart_tlsreturns a dead transport.Fix: start reading before calling
connection_made. No read callback can fire until control returns to the loop, so the ordering stays the same, and a pause fromconnection_madenow actually applies:Tested: I rebuilt 0.23.0 with Cython (regenerated
loop.c) and applied the patch. Your script now printsstart_tls raised ConnectionResetErrorin all four configurations, and the protocol snippet above no longer receives data while paused.test_tcp,test_unix,test_pipes,test_udp,test_base,test_contextandtest_sockets: 316 passed / 28 skipped, the same as an unpatched build.basetransport.pyxis identical on master.