aio-libs/aiohttp · error · RuntimeError

Response has not been started

Error message

Response has not been started

What it means

Raised by WebSocketResponse.write_eof() when self._payload_writer is None. _payload_writer is set by the parent StreamResponse.prepare(); for a WebSocketResponse the real setup happens in its overridden prepare(). Calling write_eof before the response has been started (prepared) means there is no writer to flush EOF to.

Source

Thrown at aiohttp/web_ws.py:505

    async def send_json_bytes(
        self,
        data: Any,
        compress: int | None = None,
        *,
        dumps: JSONBytesEncoder,
    ) -> None:
        """Send JSON data using a bytes-returning encoder as a binary frame.

        Use this when your JSON encoder (like orjson) returns bytes
        instead of str, avoiding the encode/decode overhead.
        """
        await self.send_bytes(dumps(data), compress=compress)

    async def write_eof(self) -> None:  # type: ignore[override]
        if self._eof_sent:
            return
        if self._payload_writer is None:
            raise RuntimeError("Response has not been started")

        await self.close()
        self._eof_sent = True

    async def close(
        self, *, code: int = WSCloseCode.OK, message: bytes = b"", drain: bool = True
    ) -> bool:
        """Close websocket connection."""
        if self._writer is None:
            raise RuntimeError("Call .prepare() first")

        if self._closed:
            return False
        self._set_closed()

        try:
            await self._writer.close(code, message)
            writer = self._payload_writer

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Ensure `await ws.prepare(request)` completes successfully before any code path that may call write_eof().
  2. In generic middleware, check `ws.prepared` (or `ws._payload_writer is not None`) before calling write_eof().
  3. Let the normal close() path handle end-of-stream instead of calling write_eof() directly.

Example fix

# before
ws = web.WebSocketResponse()
await ws.write_eof()

# after
ws = web.WebSocketResponse()
await ws.prepare(request)
await ws.write_eof()  # delegates to close()
Defensive patterns

Strategy: validation

Validate before calling

async def safe_write_eof(ws: web.WebSocketResponse) -> None:
    if ws._payload_writer is None:
        return  # nothing started, nothing to flush
    await ws.write_eof()

Type guard

def response_started(ws) -> bool:
    return getattr(ws, "_payload_writer", None) is not None

Try / catch

try:
    await ws.write_eof()
except RuntimeError as e:
    if "Response has not been started" not in str(e):
        raise

Prevention

When it happens

Trigger: Invoking `await ws.write_eof()` on a WebSocketResponse that never had `await ws.prepare(request)` called; or framework/middleware code that calls write_eof() on a response object whose prepare() was skipped or failed before assigning the writer.

Common situations: Middleware or cleanup hooks that unconditionally call write_eof() on any response; a handler that returns early before prepare(); an exception path that aborts after constructing the WebSocketResponse but before prepare() completes.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/485b58954e65fe4a.json. Report an issue: GitHub.