Repository navigation
fix(stt): notice a half-open websocket in the baseten, fireworksai and gradium streams - #7943
Rehansanjay wants to merge 1 commit into
Conversation
…d gradium streams Follow-up to livekit#7913 and livekit#7914. In livekit#7914 I claimed these three were already safe because they bound the read with `asyncio.wait_for(ws.receive(), timeout=5)`. They do call it, but the handler is: except asyncio.TimeoutError: if closing_ws: break continue That is a polling timeout, not a liveness bound: it fires every five seconds, the loop carries on, and a socket that died without a FIN/RST is never escalated. I read the `wait_for` and never checked what the handler did with it. Measured, rather than argued. livekit#7900's probe extended to every streaming STT plugin, against a local server that accepts the upgrade and then answers nothing, pings included: before after baseten 75s (never) 48.1s fireworksai 75s (never) 47.0s gradium 75s (never) 47.0s against 46.4s / 46.9s / 47.0s for smallestai, telnyx and deepgram, which already pass a heartbeat. The harness agrees with livekit#7900 on deepgram: 47.0s against his 45.4s. Each gets `heartbeat=30.0`, matching the eleven plugins that already have it, and a raising ERROR branch -- their `not TEXT` branches logged the frame and continued, which is the hole livekit#7913 found elsewhere, so the heartbeat alone would have detected the dead socket and still died quietly. One existing test needed widening: gradium's fake session took `ws_connect(self, url, headers)` positionally and now sees the new kwarg.
There was a problem hiding this comment.
🔍 Devin Review: 1 flag
Not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
|
The measurement behind this is now a repo rather than a number in a comment: https://github.com/Rehansanjay/livekit-stt-halfopen It is @DEVJHAWAR11's probe from #7900 extended to all nineteen streaming STT plugins, with the harness, the per-plugin construction table and the raw results. Against The mechanism turns out to be one parameter. Every plugin that raised passes Re-runnable from any |
|
Thanks! Please remove the three test_socket_is_opened_with_a_heartbeat tests — they only check a kwarg on a half-built stream with exceptions suppressed; a half-open-socket test like #8023's would be welcome instead. |
|
Thanks @Rehansanjay for this! #8123 added a built-in liveness monitor to these STT websockets, which covers this, so closing. |
Follow-up to #7913 and #7914, and a correction: in #7914 I listed these three as not vulnerable. They are.
What I got wrong
I wrote that baseten, fireworksai and gradium bound the read with
asyncio.wait_for(ws.receive(), timeout=5)and were therefore safe. They do call it. The handler is:A polling timeout, not a liveness bound. It fires every five seconds, the loop carries on, and a socket that died without a FIN/RST is never escalated. I read the
wait_forand never checked what the handler did with it.Measured rather than argued
I extended @DEVJHAWAR11's probe from #7900 to every streaming STT plugin in the tree. A local server accepts the upgrade and then answers nothing, pings included; each plugin is fed silence and timed until it raises. No API keys, no network.
For reference, the plugins that already pass a heartbeat: smallestai 46.4s, telnyx 46.9s, deepgram 47.0s. The harness agrees with #7900's number for deepgram — 47.0s against his 45.4s.
The change
heartbeat=30.0on each connect, matching the eleven plugins that already have it, plus a raising ERROR branch. That second half matters: all three sent an ERROR frame into theirnot TEXTbranch, which logs and continues, so the receive loop would end normally,_runwould break instead of reconnecting, and the base stream only reports a failure when_runraises. The heartbeat alone would have detected the dead socket and the stream would still have died quietly — the same hole #7913 found in inworld, openai and elevenlabs.Tests
One per plugin asserting the socket is opened with the heartbeat, following
tests/test_plugin_soniox_stt.py. gradium's goes in its existing file; baseten and fireworksai had no STT test module.One existing test needed widening rather than fixing: gradium's fake session declared
ws_connect(self, url, headers)positionally and now sees the new kwarg.The before/after measurement above is the real verification — it exercises the whole receive path against a genuinely half-open socket, which a unit test cannot.
Two things the measurement found that are not this PR's business
soniox and simplismart detect the dead socket and never surface it. Soniox's own log:
It detects at ~46s, reconnects, and repeats indefinitely.
conn_options.max_retry=0does not bound it, because that is the plugin's own reconnect loop rather than the base class's retry — so aFallbackAdapteris never told and the caller stays deaf. simplismart logs "connection lost" at ~50.6s and likewise never raises. Whether an unbounded internal reconnect should ever surface failure reads as a policy question for a maintainer, so I have left both alone.Six plugins could not be measured, and I would rather report that than guess: meta's constructor requires an absolute
wss://; slng's endpoint validation rejected every local shape I tried; sarvam's session handshake answers 403 against my local server; microsoft-ai, nabrah and xai expose no endpoint override at all.