aio-libs/aiohttp · error · RuntimeError

Call .prepare() first

Error message

Call .prepare() first

What it means

WebSocketResponse.ping() raises RuntimeError if self._writer is None — i.e. prepare() has not been called yet (or failed). ping() needs the WebSocketWriter to send a frame over the live connection. This is the same _writer sentinel used throughout. Until prepare() succeeds, there is no transport to send on.

Solutions

  1. Always call and await ws.prepare(request) before any ping/pong/send_*.
  2. Guard with: if not ws.prepared: return, or check ws.prepared before pinging.
  3. Spawn heartbeat tasks only after prepare() has returned successfully.

Example fix

// before
async def handler(request):
    ws = web.WebSocketResponse()
    await ws.ping()  # raises
    await ws.prepare(request)
// after
async def handler(request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)
    await ws.ping()
Defensive patterns

Strategy: validation

Validate before calling

async def safe_ping(ws):
    if not ws.prepared:
        return
    await ws.ping()

Type guard

def ws_ready_for_control(ws: web.WebSocketResponse) -> bool:
    return ws.prepared and not ws.closed

Try / catch

try:
    await ws.ping()
except RuntimeError as e:
    if 'prepare' in str(e).lower():
        log.debug('ping before prepare; skipping')
    else:
        raise

Prevention

When it happens

Trigger: Calling await ws.ping() before await ws.prepare(request); calling ping() in a code path where prepare() raised or was never reached; background tasks started before the handshake completed.

Common situations: Sending pings from a heartbeat task spawned before prepare(); refactoring that moved prepare() below the first ping; conditional prepare() where a branch skips it but still calls ping().

Related errors


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

Appendix: source

Thrown at aiohttp/web_ws.py:445

    def compress(self) -> int | bool:
        return self._compress

    def get_extra_info(self, name: str, default: Any = None) -> Any:
        """Get optional transport information.

        If no value associated with ``name`` is found, ``default`` is returned.
        """
        writer = self._writer
        if writer is None:
            return default
        return writer.transport.get_extra_info(name, default)

    def exception(self) -> BaseException | None:
        return self._exception

    async def ping(self, message: bytes = b"") -> None:
        if self._writer is None:
            raise RuntimeError("Call .prepare() first")
        await self._writer.send_frame(message, WSMsgType.PING)

    async def pong(self, message: bytes = b"") -> None:
        # unsolicited pong
        if self._writer is None:
            raise RuntimeError("Call .prepare() first")
        await self._writer.send_frame(message, WSMsgType.PONG)

    async def send_frame(
        self, message: bytes, opcode: WSMsgType, compress: int | None = None
    ) -> None:
        """Send a frame over the websocket."""
        if self._writer is None:
            raise RuntimeError("Call .prepare() first")
        await self._writer.send_frame(message, opcode, compress)

    async def send_str(self, data: str, compress: int | None = None) -> None:
        if self._writer is None:

View on GitHub (pinned to d041d4d0fd)