add WebSocket reliability: idle timeout, rate limiting, connection limits (#1204)
* add WebSocket reliability: idle timeout, rate limiting, connection limits
- add idle timeout (5 min) — closes stale connections with 4008
- add per-connection rate limiting (20 msg/sec sliding window)
- add per-jam connection limit (50) — prevents resource exhaustion
- validate incoming WS messages with pydantic before dispatching
- fix fan-out to call disconnect_ws() for dead connections (cleans up
_ws_by_did, _ws_client_ids, and triggers output device fallback)
- categorize disconnect exceptions (normal vs send errors vs unexpected)
- add frontend ping interval (60s) to keep connections alive
- add 4008/4009 to frontend terminal close codes (no reconnect)
- add tests: idle timeout, rate limiting, connection limit, invalid
JSON, invalid message format, command round-trip via WS
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix CI: use .code attribute for WebSocketDisconnect assertions
WebSocketDisconnect.__str__() is empty so regex match never works.
Check the .code attribute directly instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* relax loq limit for test_jams.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix flaky test_ws_command_round_trip: consume messages until expected state
The stream reader processes commands asynchronously via Redis Streams,
so intermediate messages may arrive before the state update.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* narrow contextlib.suppress to specific exceptions for ws.close()
ws.close() on a dead connection raises RuntimeError (already disconnected)
or WebSocketDisconnect (transport died during send). No reason to suppress
anything broader.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix flaky idle timeout test: accept 4008 or 1011 close code
asyncio.wait_for cancellation races with the close frame delivery in
the synchronous TestClient, so the transport may report 1011 instead
of the intended 4008. Both confirm the server killed the idle connection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
authored by