{"id":"19ac3fe2fc963c74","repo":"aio-libs/aiohttp","slug":"cannot-write-to-closing-transport","errorCode":null,"errorMessage":"Cannot write to closing transport","messagePattern":"Cannot write to closing transport","errorType":"exception","errorClass":"ClientConnectionResetError","httpStatus":null,"severity":"error","filePath":"aiohttp/_websocket/writer.py","lineNumber":78,"sourceCode":"        self.protocol = protocol\n        self.transport = transport\n        self.use_mask = use_mask\n        self.get_random_bits = partial(random.getrandbits, 32)\n        self.compress = compress\n        self.notakeover = notakeover\n        self._closing = False\n        self._limit = limit\n        self._output_size = 0\n        self._compressobj: ZLibCompressor | None = None\n        self._send_lock = asyncio.Lock()\n        self._background_tasks: set[asyncio.Task[None]] = set()\n\n    async def send_frame(\n        self, message: bytes, opcode: int, compress: int | None = None\n    ) -> None:\n        \"\"\"Send a frame over the websocket with message as its payload.\"\"\"\n        if self._closing and not (opcode & WSMsgType.CLOSE):\n            raise ClientConnectionResetError(\"Cannot write to closing transport\")\n\n        if not (compress or self.compress) or opcode >= WS_CONTROL_FRAME_OPCODE:\n            # Non-compressed frames don't need lock or shield\n            self._write_websocket_frame(message, opcode, 0)\n        elif len(message) <= WEBSOCKET_MAX_SYNC_CHUNK_SIZE:\n            # Small compressed payloads - compress synchronously in event loop\n            # We need the lock even though sync compression has no await points.\n            # This prevents small frames from interleaving with large frames that\n            # compress in the executor, avoiding compressor state corruption.\n            async with self._send_lock:\n                self._send_compressed_frame_sync(message, opcode, compress)\n        else:\n            # Large compressed frames need shield to prevent corruption\n            # For large compressed frames, the entire compress+send\n            # operation must be atomic. If cancelled after compression but\n            # before send, the compressor state would be advanced but data\n            # not sent, corrupting subsequent frames.\n            # Create a task to shield from cancellation","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/c0ef574e29109210e96e652771ae4e7b88615fa4/aiohttp/_websocket/writer.py#L60-L96","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nawait ws.close()\nawait ws.send_str('done')  # raises ClientConnectionResetError\n\n# after\nif not ws.closed:\n    await ws.send_str('done')\nawait ws.close()","handlingStrategy":"validation","validationCode":"if not ws.closed:\n    await ws.send_str(payload)","typeGuard":null,"tryCatchPattern":"try:\n    await ws.send_str(payload)\nexcept aiohttp.ClientConnectionResetError:\n    pass  # already closing","preventionTips":["Coordinate close() and senders with an asyncio.Event/lock","Check ws.closed before sending","Stop producer tasks before closing the websocket"],"tags":["websocket","writer","connection-reset","lifecycle","concurrency"],"analyzedSha":"c0ef574e29109210e96e652771ae4e7b88615fa4","analyzedAt":"2026-08-04T19:51:05.467Z","schemaVersion":2}