ruvnet/RuView · error · RuntimeError

SensingClient not connected. Use `async with` first.

Error message

SensingClient not connected. Use `async with` first.

What it means

SensingClient.stream() iterates self._ws, which is only assigned in __aenter__ (via websockets.connect) and reset to None by close(). Calling stream() on a client that never entered its `async with` block — or already exited it — raises RuntimeError with the usage hint.

Source

Thrown at python/wifi_densepose/client/ws.py:270

    async def close(self) -> None:
        """Idempotent connection close."""
        if self._ws is not None:
            try:
                await self._ws.close()
            except Exception as e:  # pragma: no cover — best-effort close
                log.debug("ignored WS close error: %r", e)
            self._ws = None

    async def stream(self) -> AsyncIterator[SensingMessage]:
        """Yield typed messages until the server closes the connection
        or the context is exited.

        Decode failures on individual frames are logged at WARN and
        swallowed — a malformed frame should not terminate the stream
        (the next frame may be fine)."""
        if self._ws is None:
            raise RuntimeError("SensingClient not connected. Use `async with` first.")
        try:
            async for frame in self._ws:
                if isinstance(frame, bytes):
                    frame = frame.decode("utf-8", errors="replace")
                try:
                    yield _decode(frame)
                except (ValueError, json.JSONDecodeError) as e:
                    log.warning("dropping malformed sensing-server frame: %r", e)
        except ConnectionClosed:
            # Graceful EOF — exit the iterator normally.
            return

    async def send_ping(self) -> None:
        """Send an application-level ping. The sensing-server replies
        with `{"type": "pong"}` (main.rs:2698)."""
        if self._ws is None:
            raise RuntimeError("SensingClient not connected. Use `async with` first.")
        await self._ws.send(json.dumps({"type": "ping"}))

View on GitHub (pinned to 4685618388)

Solutions

  1. Wrap usage in the async context manager: `async with SensingClient(url) as client: ...`
  2. Only iterate stream() while the context is active; treat close() as terminal
  3. For reconnection, wrap the whole `async with` in an application-level retry loop (the client intentionally does not auto-reconnect)

Example fix

# before
client = SensingClient("ws://localhost:8765/ws/sensing")
async for msg in client.stream():  # RuntimeError: not connected
    ...

# after
async with SensingClient("ws://localhost:8765/ws/sensing") as client:
    async for msg in client.stream():
        ...
Defensive patterns

Strategy: validation

Validate before calling

def sensing_connected(client) -> bool:
    """True while inside an `async with` block (connection is open)."""
    return client._ws is not None

# only stream while the context is active:
async with SensingClient(url) as client:
    assert sensing_connected(client)
    async for msg in client.stream():
        ...

Try / catch

try:
    async for msg in client.stream():
        ...
except RuntimeError as e:
    if "not connected" in str(e):
        raise RuntimeError(
            "stream() must run inside `async with SensingClient(...)`"
        ) from e
    raise

Prevention

When it happens

Trigger: `client = SensingClient(url)` followed by `async for msg in client.stream():` without `async with client:`; or calling stream() after close()/__aexit__ set _ws back to None.

Common situations: Refactoring away from a manual connect() API; copy-pasting from synchronous examples; a task started before the context manager was entered.

Related errors


AI-assisted analysis of ruvnet/RuView@4685618388 (2026-08-16). Data as JSON: /api/errors/9b4249617dcb4902. Report an issue: GitHub.