aio-libs/aiohttp · error · ClientConnectionResetError
Cannot write to closing transport
Error message
Cannot write to closing transport
What it means
Raised by WebSocketWriter.send_frame (writer.py:78) when self._closing is already True and the opcode being sent is not CLOSE. Once close() has been called, the writer forbids any further application data/control frames, raising ClientConnectionResetError. This prevents writing into a half-closed socket.
Source
Thrown at aiohttp/_websocket/writer.py:78
self.protocol = protocol
self.transport = transport
self.use_mask = use_mask
self.get_random_bits = partial(random.getrandbits, 32)
self.compress = compress
self.notakeover = notakeover
self._closing = False
self._limit = limit
self._output_size = 0
self._compressobj: ZLibCompressor | None = None
self._send_lock = asyncio.Lock()
self._background_tasks: set[asyncio.Task[None]] = set()
async def send_frame(
self, message: bytes, opcode: int, compress: int | None = None
) -> None:
"""Send a frame over the websocket with message as its payload."""
if self._closing and not (opcode & WSMsgType.CLOSE):
raise ClientConnectionResetError("Cannot write to closing transport")
if not (compress or self.compress) or opcode >= WS_CONTROL_FRAME_OPCODE:
# Non-compressed frames don't need lock or shield
self._write_websocket_frame(message, opcode, 0)
elif len(message) <= WEBSOCKET_MAX_SYNC_CHUNK_SIZE:
# Small compressed payloads - compress synchronously in event loop
# We need the lock even though sync compression has no await points.
# This prevents small frames from interleaving with large frames that
# compress in the executor, avoiding compressor state corruption.
async with self._send_lock:
self._send_compressed_frame_sync(message, opcode, compress)
else:
# Large compressed frames need shield to prevent corruption
# For large compressed frames, the entire compress+send
# operation must be atomic. If cancelled after compression but
# before send, the compressor state would be advanced but data
# not sent, corrupting subsequent frames.
# Create a task to shield from cancellationView on GitHub (pinned to c0ef574e29)
Solutions
- Check ws.closed / ws._closing before sending, or coordinate close with an asyncio.Event so senders stop first.
- Guard concurrent close+send with a lock so close wins and senders abort cleanly.
- Catch ClientConnectionResetError around sends after a close to tolerate the race.
Example fix
# before
await ws.close()
await ws.send_str('done') # raises ClientConnectionResetError
# after
if not ws.closed:
await ws.send_str('done')
await ws.close() Defensive patterns
Strategy: validation
Validate before calling
if not ws.closed:
await ws.send_str(payload) Try / catch
try:
await ws.send_str(payload)
except aiohttp.ClientConnectionResetError:
pass # already closing Prevention
- Coordinate close() and senders with an asyncio.Event/lock
- Check ws.closed before sending
- Stop producer tasks before closing the websocket
When it happens
Trigger: Application code calls ws.close() (which sets _closing=True) and then attempts another ws.send_str/ws.send_bytes/ws.ping afterwards; or two tasks race where one closes while the other sends.
Common situations: A receive loop that closes on error and a concurrent sender that has not yet observed the close; cleanup paths that send after close; missing checks of ws.closed.
Related errors
- Concurrent call to receive() is not allowed
- Cannot write to closing transport
- Response has not been started
- Concurrent call to receive() is not allowed
- WebSocket connection is closed.
AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04).
Data as JSON: /data/errors/19ac3fe2fc963c74.json.
Report an issue: GitHub.