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 StreamResponse.prepare() (the parent class), i.e. the HTTP response body must have been started. write_eof is normally invoked by the framework during connection teardown; reaching it with no payload writer means teardown ran on a response that never started.

Solutions

  1. Ensure `await ws.prepare(request)` completes before any code path can return the response.
  2. Do not call ws.write_eof() yourself — the framework closes the WebSocket via ws.close(); call close() instead.
  3. If you catch exceptions around prepare(), re-raise or return a plain HTTP response instead of the unprepared ws.
  4. Inspect ws.prepared before any manual EOF/close path.

Example fix

// before
async def handler(request):
    ws = web.WebSocketResponse()
    try:
        await ws.prepare(request)
    except Exception:
        pass
    return ws  # framework calls write_eof -> raises

// after
async def handler(request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)
    ...
    await ws.close()
    return ws
Defensive patterns

Strategy: validation

Validate before calling

if ws.prepared:
    await ws.write_eof()
# better: do not call write_eof yourself; rely on the framework / ws.close()

Type guard

def ws_has_payload(ws: web.WebSocketResponse) -> bool:
    return getattr(ws, '_payload_writer', None) is not None

Try / catch

try:
    await ws.write_eof()
except RuntimeError as e:
    if 'not been started' in str(e):
        pass  # nothing to end
    else:
        raise

Prevention

When it happens

Trigger: The server / runner calls write_eof() on a WebSocketResponse whose prepare() raised or was never awaited; an explicit `await ws.write_eof()` in user code before prepare(); connection cleanup racing after a handshake failure.

Common situations: Catching and swallowing an exception from prepare() then letting the handler return the ws (framework then tries to finalize/write_eof); a custom runner or middleware that writes EOF unconditionally.

Related errors


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

Appendix: 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 d041d4d0fd)