Bug
WSServerHandshakeError extends aiohttp.ClientResponseError, so it is neither an aiohttp.ClientConnectorError nor an asyncio.TimeoutError. Six credentialed ws_connect sites still wrap the connect in except (aiohttp.ClientConnectorError, asyncio.TimeoutError), so a rejected upgrade (401/403 from a bad, expired or rate-limited key) matches neither handler and escapes uncaught. It surfaces as an unretrieved task exception whose repr embeds RequestInfo, including the auth header.
That is the same failure #6739 reported against cartesia and #6740 fixed there. The reporter on that issue wondered whether it was "the cartesia plugin, or more g[enerally]"; these are the sites where it is still reachable.
| plugin |
file |
| deepgram |
stt.py, stt_v2.py |
| simplismart |
stt.py |
| smallestai |
stt.py |
| xai |
stt.py, tts.py |
Each sends the key on the websocket (deepgram, for example, headers={"Authorization": f"Token {self._api_key}"} in stt.py) and then catches only the two error types above. I checked the other credentialed ws_connect sites by walking each try block's handlers; the remaining ten already catch the response error or re-raise with from None.
Steps to reproduce
Deepgram's exact call shape against a server that refuses the upgrade:
import asyncio
import aiohttp
from aiohttp import web
FAKE_KEY = "dg_live_SUPERSECRET_abcdef0123456789"
async def reject(request):
return web.Response(status=401, text="Unauthorized")
async def main():
app = web.Application()
app.router.add_get("/v1/listen", reject)
runner = web.AppRunner(app)
await runner.setup()
await web.TCPSite(runner, "127.0.0.1", 8781).start()
session = aiohttp.ClientSession()
try:
await asyncio.wait_for(
session.ws_connect(
"http://127.0.0.1:8781/v1/listen?model=nova-2",
headers={"Authorization": f"Token {FAKE_KEY}"},
),
10,
)
except (aiohttp.ClientConnectorError, asyncio.TimeoutError):
print("caught by the plugin's handler")
except Exception as e:
print(type(e).__name__, "escaped;", "key in repr:", FAKE_KEY in repr(e))
print(repr(e)[:220])
finally:
await session.close()
await runner.cleanup()
asyncio.run(main())
WSServerHandshakeError escaped; key in repr: True
WSServerHandshakeError(RequestInfo(url=URL('http://127.0.0.1:8781/v1/listen?model=nova-2'), method='GET',
headers=<CIMultiDictProxy('Host': '127.0.0.1:8781', 'Authorization': 'Token dg_live_SUPERSECRET_abcdef0123456789',
A 401 is the ordinary case here, not a contrived one: any wrong, rotated or rate-limited key produces it, and the reconnect path retries, so it can repeat.
Expected
No credential material in logs or exception output.
The shape #6740 used for cartesia ports directly: catch the response error separately and re-raise with from None, surfacing type(e).__name__ rather than str(e) or repr(e).
Note
Filing publicly because #6739 was handled that way and there is no SECURITY.md in this repo or on the livekit org. Happy to move it if you would rather it went through a private channel. I have the patch and can open a PR.
Version
main @ cdb37ad
Bug
WSServerHandshakeErrorextendsaiohttp.ClientResponseError, so it is neither anaiohttp.ClientConnectorErrornor anasyncio.TimeoutError. Six credentialedws_connectsites still wrap the connect inexcept (aiohttp.ClientConnectorError, asyncio.TimeoutError), so a rejected upgrade (401/403 from a bad, expired or rate-limited key) matches neither handler and escapes uncaught. It surfaces as an unretrieved task exception whose repr embedsRequestInfo, including the auth header.That is the same failure #6739 reported against cartesia and #6740 fixed there. The reporter on that issue wondered whether it was "the cartesia plugin, or more g[enerally]"; these are the sites where it is still reachable.
stt.py,stt_v2.pystt.pystt.pystt.py,tts.pyEach sends the key on the websocket (deepgram, for example,
headers={"Authorization": f"Token {self._api_key}"}instt.py) and then catches only the two error types above. I checked the other credentialedws_connectsites by walking eachtryblock's handlers; the remaining ten already catch the response error or re-raise withfrom None.Steps to reproduce
Deepgram's exact call shape against a server that refuses the upgrade:
A 401 is the ordinary case here, not a contrived one: any wrong, rotated or rate-limited key produces it, and the reconnect path retries, so it can repeat.
Expected
No credential material in logs or exception output.
The shape #6740 used for cartesia ports directly: catch the response error separately and re-raise with
from None, surfacingtype(e).__name__rather thanstr(e)orrepr(e).Note
Filing publicly because #6739 was handled that way and there is no
SECURITY.mdin this repo or on the livekit org. Happy to move it if you would rather it went through a private channel. I have the patch and can open a PR.Version
main @ cdb37ad