From 38c78fb5758807ddfd01b9826cb2854c949e8222 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Sat, 28 Mar 2026 08:50:43 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20WebSocket=20proxy=20drops=20text=20frame?= =?UTF-8?q?s=20=E2=80=94=20receive=5Fbytes()=20silently=20fails=20on=20aut?= =?UTF-8?q?h=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit terminal.js sends the ttyd auth token as a TEXT WebSocket frame. The proxy's client_to_ttyd() called receive_bytes() which fails silently on text frames, swallows the exception, and exits — auth never reached ttyd, ttyd never started streaming, resulting in permanent black screen and reconnect loop. Fix: use receive() and dispatch on message type, handling both bytes and text. --- muxplex/main.py | 7 +++++-- muxplex/tests/test_ws_proxy.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 muxplex/tests/test_ws_proxy.py diff --git a/muxplex/main.py b/muxplex/main.py index 9ce87d6..59b84c5 100644 --- a/muxplex/main.py +++ b/muxplex/main.py @@ -365,8 +365,11 @@ async def terminal_ws_proxy(websocket: WebSocket) -> None: async def client_to_ttyd() -> None: try: while True: - data = await websocket.receive_bytes() - await ttyd_ws.send(data) + msg = await websocket.receive() + if msg.get("bytes"): + await ttyd_ws.send(msg["bytes"]) + elif msg.get("text"): + await ttyd_ws.send(msg["text"]) except Exception: pass diff --git a/muxplex/tests/test_ws_proxy.py b/muxplex/tests/test_ws_proxy.py new file mode 100644 index 0000000..00c2fee --- /dev/null +++ b/muxplex/tests/test_ws_proxy.py @@ -0,0 +1,28 @@ +""" +Regression tests for the WebSocket proxy in muxplex/main.py. +""" + +import inspect + +from muxplex.main import terminal_ws_proxy + + +def test_terminal_ws_proxy_does_not_use_receive_bytes(): + """Regression: receive_bytes() silently drops TEXT frames (like the ttyd auth token). + + terminal.js sends {"AuthToken": ""} as a TEXT WebSocket frame. The original + proxy used receive_bytes() which fails on text frames, swallowed the exception, + and exited — meaning ttyd never received the auth token, never started + streaming, resulting in a permanent black screen and reconnect loop. + + The proxy MUST use receive() and dispatch on message type to handle both + binary and text frames correctly. + """ + source = inspect.getsource(terminal_ws_proxy) + assert "receive_bytes" not in source, ( + "client_to_ttyd must not use receive_bytes() — silently drops text frames " + 'like the ttyd auth token {"AuthToken": ""}' + ) + assert ".receive()" in source, ( + "client_to_ttyd must use receive() to handle both text and binary frames" + )