aio-libs/aiohttp · error · RuntimeError

Already started

Error message

Already started

What it means

WebSocketResponse.can_prepare() raises RuntimeError('Already started') if self._writer is already set — meaning prepare()/_post_start() has already run for this WebSocketResponse instance. A single WebSocketResponse is a stateful object tied to one connection lifecycle; calling can_prepare() (or prepare()) twice is a programmer error. The check uses the same _writer sentinel as the prepared property.

Solutions

  1. Create a fresh WebSocketResponse() per request: ws = web.WebSocketResponse() inside the handler.
  2. Call can_prepare() and prepare() exactly once per request — guard with the prepared property if needed.
  3. Don't share a WebSocketResponse across requests or store it on a long-lived object.

Example fix

// before — reused module-level ws
_WS = web.WebSocketResponse()
async def handler(request):
    if _WS.can_prepare(request).ok:  # raises second time
        await _WS.prepare(request)
// after
async def handler(request):
    ws = web.WebSocketResponse()
    if ws.can_prepare(request).ok:
        await ws.prepare(request)
Defensive patterns

Strategy: validation

Validate before calling

async def handler(request):
    ws = web.WebSocketResponse()  # fresh per request
    if ws.prepared:  # always False here; sanity check
        raise RuntimeError('reuse of WebSocketResponse')
    ready = ws.can_prepare(request)
    if not ready.ok:
        return web.Response(status=400)
    await ws.prepare(request)
    # ...

Type guard

def ws_is_fresh(ws: web.WebSocketResponse) -> bool:
    return not ws.prepared

Try / catch

ws = web.WebSocketResponse()
try:
    ready = ws.can_prepare(request)
except RuntimeError as e:
    if 'Already started' in str(e):
        raise RuntimeError('WebSocketResponse reused across requests — create a new one per handler call')
    raise

Prevention

When it happens

Trigger: Calling await ws.can_prepare(request) after await ws.prepare(request); reusing the same WebSocketResponse object across two requests; a handler that calls can_prepare() then unconditionally calls prepare() again inside an if-branch that already prepared.

Common situations: Middleware and handler both calling prepare(); reusing a module-level WebSocketResponse; refactoring that moved prepare() but left a duplicate; buggy reconnection logic reusing the WS object.

Related errors


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

Appendix: source

Thrown at aiohttp/web_ws.py:402

        loop = self._loop
        assert loop is not None
        self._reader = WebSocketDataQueue(
            request._protocol, DEFAULT_CHUNK_SIZE, loop=loop
        )
        parser = WebSocketReader(
            self._reader,
            self._max_msg_size,
            compress=bool(self._compress),
            decode_text=self._decode_text,
        )
        cb = None if self._heartbeat is None else self._on_data_received
        request.protocol.set_parser(parser, data_received_cb=cb)
        # disable HTTP keepalive for WebSocket
        request.protocol.keep_alive(False)

    def can_prepare(self, request: BaseRequest) -> WebSocketReady:
        if self._writer is not None:
            raise RuntimeError("Already started")
        try:
            _, protocol, _, _ = self._handshake(request)
        except HTTPException:
            return WebSocketReady(False, None)
        else:
            return WebSocketReady(True, protocol)

    @property
    def prepared(self) -> bool:
        return self._writer is not None

    @property
    def closed(self) -> bool:
        return self._closed

    @property
    def close_code(self) -> int | None:
        return self._close_code

View on GitHub (pinned to d041d4d0fd)