Description
Bug report
Bug description:
SelectorSocketTransport (in asyncio.selector_events) implements both write
and writelines
, but only the first has handling for the case that the connection has already been lost. That in turn can lead to dereferencing a None object, because _call_connection_lost
clears a number of attributes. Even if that were fixed, it also means that there would be inconsistent behaviour between write
and writelines
(write
would drop the data on the floor and possibly print a warning, while writelines
would append it to the buffer).
I would suggest modifying writelines
to use the same check for _conn_lost
as write
. Let me know if you'd like a PR.
The code below gives either an AttributeError
or TypeError
(depending on Python version) on my machine (Ubuntu 24.04, 6.8.0-63-generic). It may depend on corner cases of when exactly the kernel notifies a TCP sender that the other side has closed the connection and hence might not be a totally reliable reproducer.
#!/usr/bin/env python3
import asyncio
import functools
async def client_connected_cb(event: asyncio.Event, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
await event.wait() # Waits for the client side to disconnect
writer.write(b"foo")
writer.write(b"spam") # Linux seems to fail the second write but not the first
await asyncio.sleep(0)
print(writer.transport._conn_lost) # Should be 1
writer.writelines([b"baz"])
# Replace above line with this for comparison:
# writer.write(b"baz")
await writer.drain()
writer.close()
await writer.wait_closed()
async def main():
event = asyncio.Event()
server = await asyncio.start_server(
functools.partial(client_connected_cb, event),
host="127.0.0.1",
port=8888,
)
# Establish a connection, then close it
client_reader, client_writer = await asyncio.open_connection("127.0.0.1", 8888)
client_writer.close()
await client_writer.wait_closed()
event.set() # Wake up client_connected_cb
# Cleanup
server.close()
await server.wait_closed()
if __name__ == "__main__":
asyncio.run(main())
Output (3.12):
1
Unhandled exception in client_connected_cb
transport: <_SelectorSocketTransport closed fd=8>
Traceback (most recent call last):
File "/home/bmerry/work/experiments/asyncio-writelines/./writelines_hang.py", line 13, in client_connected_cb
writer.writelines([b"baz"])
File "/usr/lib/python3.12/asyncio/streams.py", line 349, in writelines
self._transport.writelines(data)
File "/usr/lib/python3.12/asyncio/selector_events.py", line 1185, in writelines
self._loop._add_writer(self._sock_fd, self._write_ready)
^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute '_add_writer'
Output (3.13, 3.14):
1
Unhandled exception in client_connected_cb
transport: <_SelectorSocketTransport closed fd=8>
Traceback (most recent call last):
File "/home/bmerry/work/experiments/asyncio-writelines/./writelines_hang.py", line 13, in client_connected_cb
writer.writelines([b"baz"])
~~~~~~~~~~~~~~~~~^^^^^^^^^^
File "/home/bmerry/.pyenv/versions/3.14-dev/lib/python3.14/asyncio/streams.py", line 343, in writelines
self._transport.writelines(data)
~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "/home/bmerry/.pyenv/versions/3.14-dev/lib/python3.14/asyncio/selector_events.py", line 1178, in writelines
self._write_ready()
~~~~~~~~~~~~~~~~~^^
TypeError: 'NoneType' object is not callable
Note that 3.11 and earlier are not affected because they do not implement writelines
in SelectorSocketTransport; they rely on the generic version that just concatenates the messages and passes them to write
.
CPython versions tested on:
3.13, 3.14, 3.12
Operating systems tested on:
Linux
Metadata
Metadata
Assignees
Labels
Projects
Status