{"record":{"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/d041d4d0fd48c3f0832084d33be16cf1c4835f85/aiohttp/_websocket/writer.py#L60-L96","documentation":"Raised as ClientConnectionResetError('Cannot write to closing transport') at the top of WebSocketWriter.send_frame when self._closing is already True and the frame being sent is not a CLOSE frame. The writer marks itself _closing once a close handshake begins; after that only close-completion frames are permitted. Any application send (text/binary/ping) after closing has started is rejected.","triggerScenarios":"Application code calls ws.send_str()/ws.send_bytes()/ws.ping() after ws.close() has been invoked (or after a close frame was received and the writer set _closing). The check 'self._closing and not (opcode & WSMsgType.CLOSE)' is true.","commonSituations":"A receive loop detects CLOSED/CLOSING and the send path races to send a final message; cleanup code that sends after close(); a close handshake triggered by the peer while the app is mid-send; firing-and-forgetting sends without checking connection state.","solutions":["Check ws.closed / ws.close_code before sending, or guard sends behind a connection-state flag you control.","Ensure send_str/send_bytes/pong are not called after close(); cancel pending send tasks on close.","Await close() once and stop all senders before/when it begins.","Catch ClientConnectionResetError around non-critical late sends and treat as a clean shutdown."],"exampleFix":"// before\nawait ws.close()\nawait ws.send_str('done')   // ClientConnectionResetError\n// after\nif not ws.closed:\n    await ws.send_str('done')\nawait ws.close()","handlingStrategy":"validation","validationCode":"// Guard application sends behind connection state before writing.\nif not ws.closed:\n    await ws.send_str(data)\n","typeGuard":"def can_send(ws) -> bool:\n    return not ws.closed and not getattr(ws, '_closing', False)\n","tryCatchPattern":"try:\n    await ws.send_str(data)\nexcept ClientConnectionResetError:\n    # connection is closing; abandon the send\n    pass\n","preventionTips":["Never call send_str/send_bytes/ping after close().\n","Cancel pending send tasks when initiating or detecting close.","Centralize sends through one function that checks ws.closed first."],"tags":["websocket","writer","closing","connection-reset","race-condition"],"backgroundTag":null,"analyzedSha":"d041d4d0fd48c3f0832084d33be16cf1c4835f85","analyzedAt":"2026-08-11T20:44:15.550Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}