{"id":"721d7622bbd1a9e6","repo":"aio-libs/aiohttp","slug":"cannot-call-write-after-write-eof","errorCode":null,"errorMessage":"Cannot call write() after write_eof()","messagePattern":"Cannot call write\\(\\) after write_eof\\(\\)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"aiohttp/web_response.py","lineNumber":457,"sourceCode":"        assert writer is not None\n        # status line\n        version = request.version\n        status_line = f\"HTTP/{version[0]}.{version[1]} {self._status} {self._reason}\"\n        await writer.write_headers(status_line, self._headers)\n\n        # Send headers immediately if not opted into buffering\n        if self._send_headers_immediately:\n            writer.send_headers()\n\n    async def write(\n        self, data: Union[bytes, bytearray, \"memoryview[int]\", \"memoryview[bytes]\"]\n    ) -> None:\n        assert isinstance(\n            data, (bytes, bytearray, memoryview)\n        ), \"data argument must be byte-ish (%r)\" % type(data)\n\n        if self._eof_sent:\n            raise RuntimeError(\"Cannot call write() after write_eof()\")\n        if self._payload_writer is None:\n            raise RuntimeError(\"Cannot call write() before prepare()\")\n\n        await self._payload_writer.write(data)\n\n    async def drain(self) -> None:\n        assert not self._eof_sent, \"EOF has already been sent\"\n        assert self._payload_writer is not None, \"Response has not been started\"\n        warnings.warn(\n            \"drain method is deprecated, use await resp.write()\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        await self._payload_writer.drain()\n\n    async def write_eof(self, data: bytes = b\"\") -> None:\n        assert isinstance(\n            data, (bytes, bytearray, memoryview)","sourceCodeStart":439,"sourceCodeEnd":475,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/c0ef574e29109210e96e652771ae4e7b88615fa4/aiohttp/web_response.py#L439-L475","documentation":"Once write_eof() has been called the response is finalized and the writer is torn down (self._payload_writer set to None). Calling write() afterwards is illegal because the body is already terminated; write() checks self._eof_sent at line 456 first.","triggerScenarios":"Calling await resp.write(data) after await resp.write_eof(), or after the framework has auto-finalized the response (e.g. returning from the handler after a manual write_eof, or a background task writing to an already-closed response).","commonSituations":"A cleanup/background coroutine writing to a response whose handler already returned; double-finalization; fire-and-forget tasks that outlive the request.","solutions":["Guard writes with if not resp.prepared or resp._eof_sent — better, track your own 'done' flag.","Move streaming into the handler coroutine and return only after all writes complete.","Cancel/clean up background tasks before the handler returns."],"exampleFix":"# before\nawait resp.write_eof()\nawait resp.write(b'more')  # raises RuntimeError\n\n# after\nawait resp.write(b'more')\nawait resp.write_eof()  # finalize once, last","handlingStrategy":"validation","validationCode":"async def safe_write(resp, data):\n    if resp._eof_sent:\n        return  # response already finalized\n    await resp.write(data)","typeGuard":null,"tryCatchPattern":"try:\n    await resp.write(data)\nexcept RuntimeError as e:\n    if 'after write_eof' in str(e):\n        return  # ignore late writes from background tasks","preventionTips":["Centralize all writes in the handler coroutine and return only when done.","Track your own 'finalized' flag instead of relying on internals.","Cancel background tasks before the handler returns."],"tags":["http","streaming","lifecycle","response"],"analyzedSha":"c0ef574e29109210e96e652771ae4e7b88615fa4","analyzedAt":"2026-08-04T19:51:05.467Z","schemaVersion":2}