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
- Create a fresh WebSocketResponse() per request: ws = web.WebSocketResponse() inside the handler.
- Call can_prepare() and prepare() exactly once per request — guard with the prepared property if needed.
- 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
- Create a new WebSocketResponse() inside each handler — never reuse.
- Call can_prepare()/prepare() at most once per request.
- Don't store WebSocketResponse on the Application or module level.
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
- Call .prepare() first
- Changing state of started or joined application is forbidden
- Connection lost
- Response has not been started
- WebSocket connection is closed.
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_codeView on GitHub (pinned to d041d4d0fd)