aio-libs/aiohttp · error · RuntimeError

Cannot call write() after write_eof()

Error message

Cannot call write() after write_eof()

What it means

Raised by Response.write() after write_eof() has already been called on the response, meaning the body stream was already terminated. Writing after EOF would either be silently dropped or, worse, inject bytes into a subsequent response, so aiohttp treats it as a hard error.

Solutions

  1. Do not write after returning the response from the handler — keep all writes inside the handler coroutine.
  2. Check `if not resp._eof_sent:` before writing in fire-and-forget tasks, or signal the task to stop before the handler returns.
  3. Avoid calling write_eof() manually; let the framework finalize the response.
  4. Restructure SSE/streaming code so the producer task is awaited (or properly cancelled) inside the handler scope.

Example fix

// before
async def handler(request):
    resp = StreamResponse()
    await resp.prepare(request)
    asyncio.create_task(pump(resp))  # task outlives handler
    return resp  # framework calls write_eof; pump() then write() -> RuntimeError

// after
async def handler(request):
    resp = StreamResponse()
    await resp.prepare(request)
    await pump(resp)        # fully drain before returning
    await resp.write_eof()
    return resp
Defensive patterns

Strategy: validation

Validate before calling

async def safe_write(resp, data):
    if getattr(resp, '_eof_sent', False):
        return False
    await resp.write(data)
    return True

Type guard

def can_write(resp) -> bool:
    return not getattr(resp, '_eof_sent', False)

Try / catch

try:
    await resp.write(data)
except RuntimeError:
    # response already finalized; log and stop the producer
    pass

Prevention

When it happens

Trigger: Calling `await resp.write(data)` after `await resp.write_eof()` (often indirectly via the response being returned from the handler, which triggers automatic EOF). Also triggered by a background task or callback that writes to a response whose handler already returned.

Common situations: Handler returns the response, framework calls write_eof, then an `asyncio.create_task` started inside the handler tries to stream more data. Mixing manual write_eof with returning the response. SSE/WebSocket cleanup paths that write a final frame after EOF.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/721d7622bbd1a9e6. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/web_response.py:457

        assert writer is not None
        # status line
        version = request.version
        status_line = f"HTTP/{version[0]}.{version[1]} {self._status} {self._reason}"
        await writer.write_headers(status_line, self._headers)

        # Send headers immediately if not opted into buffering
        if self._send_headers_immediately:
            writer.send_headers()

    async def write(
        self, data: Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"]
    ) -> None:
        assert isinstance(
            data, (bytes, bytearray, memoryview)
        ), "data argument must be byte-ish (%r)" % type(data)

        if self._eof_sent:
            raise RuntimeError("Cannot call write() after write_eof()")
        if self._payload_writer is None:
            raise RuntimeError("Cannot call write() before prepare()")

        await self._payload_writer.write(data)

    async def drain(self) -> None:
        assert not self._eof_sent, "EOF has already been sent"
        assert self._payload_writer is not None, "Response has not been started"
        warnings.warn(
            "drain method is deprecated, use await resp.write()",
            DeprecationWarning,
            stacklevel=2,
        )
        await self._payload_writer.drain()

    async def write_eof(self, data: bytes = b"") -> None:
        assert isinstance(
            data, (bytes, bytearray, memoryview)

View on GitHub (pinned to d041d4d0fd)