Bug report: LiveView/Phoenix apps break through tunnel (WebSocket frames dropped, session cookie stripped, redirects followed)
Summary
Using traforo to expose a Phoenix LiveView app, the LiveView socket never mounts and form login is broken. Three distinct bugs in the tunnel client (src/client.ts) combine to produce this:
- WebSocket frames arriving before the upstream connection opens are silently dropped
- The
Cookie header is stripped from WebSocket upgrade requests
- HTTP redirects are followed internally by the tunnel's
fetch, swallowing Set-Cookie and the redirect status
Environment
- traforo 0.7.2
- Phoenix 1.8 / LiveView 1.2 (any Phoenix LiveView app reproduces)
- Tunneled through
kimaki tunnel (traforo 0.7.2)
- Node 24 (client-side), Bun/Chromium for the browser repro
Bug 1 — WS frames dropped during upstream connect
In handleWsOpen (src/client.ts:431), localWs is registered in this.localWsConnections only inside the 'open' callback. Any ws_frame message that arrives before that callback runs hits handleWsFrame (src/client.ts:513), which does:
const localWs = this.localWsConnections.get(msg.connId)
if (!localWs) {
console.warn(`WS FRAME for unknown connection: ${msg.connId}`)
return
}
and drops the frame. Phoenix LiveView sends phx_join immediately after the WebSocket opens, so that first frame is always the victim. The browser then loops WS OPEN → frame dropped → timeout → reconnect forever.
Repro (100% deterministic): raw WebSocket to wss://<tunnel>/live/websocket?vsn=2.0.0, send a frame in onopen:
- Frame sent immediately after open → never delivered (server logs
WS FRAME for unknown connection, no reply)
- Same frame sent after a 1.5s delay → always delivered
Fix: buffer frames in handleWsFrame while the connection is pending, flush them inside the 'open' callback after registering localWs, and clear the buffer on error/close.
Bug 2 — Cookie header stripped from WS upgrade
handleWsOpen (src/client.ts:431-447) only forwards the Sec-WebSocket-Protocol subprotocol:
const subprotocol = msg.headers['sec-websocket-protocol']
const protocols = subprotocol ? subprotocol.split(',').map(p => p.trim()) : undefined
const localWs = new WebSocket(url, protocols)
The client's Cookie header is never forwarded to the upstream WebSocket. Phoenix LiveView requires the session cookie to authorize phx_join — without it the server rejects with {:error, %{reason: "stale"}}.
Repro: WS echo server that logs the request's Cookie header. Connect with Cookie: phx_auth=SECRET-COOKIE-123:
- Direct (
ws://localhost:PORT) → server sees COOKIE: phx_auth=SECRET-COOKIE-123
- Through tunnel → server sees
COOKIE: (none)
Fix: forward cookie (and ideally origin) headers through the ws client options:
const headers = {}
if (msg.headers['cookie']) headers['cookie'] = msg.headers['cookie']
const localWs = new WebSocket(url, protocols ? { protocols, headers } : { headers })
Bug 3 — HTTP redirects followed internally
handleHttpRequest (src/client.ts:335) uses fetch(url, ...) with no redirect option. Node's fetch follows 3xx redirects by default. A Phoenix login POST returns 302 Found + Set-Cookie + Location: /; the tunnel's fetch follows that redirect internally, and because the redirect chain's cookies are never applied to the follow-up request, the browser only ever sees the final 200 login page — the session cookie and the 302 never reach the client.
Repro: POST valid credentials to /operators/log-in on a Phoenix app:
- Direct (
http://localhost:4000) → 302 Found + Set-Cookie + Location: /
- Through tunnel →
200 (final login page), no Set-Cookie, no Location
Fix: add redirect: 'manual' to the fetch options so the 3xx + headers are returned to the client as-is.
Impact
Phoenix LiveView apps (and any client that relies on immediate WS frames, WS session cookies, or 3xx redirects — Next.js auth flows, SSE endpoints, HMR) are unusable through traforo. These were found via a Phoenix LiveView app; all three repros are deterministic and scriptable.
I have a working patch for all three in the local dist/client.js and can open a PR if useful.
Bug report: LiveView/Phoenix apps break through tunnel (WebSocket frames dropped, session cookie stripped, redirects followed)
Summary
Using traforo to expose a Phoenix LiveView app, the LiveView socket never mounts and form login is broken. Three distinct bugs in the tunnel client (
src/client.ts) combine to produce this:Cookieheader is stripped from WebSocket upgrade requestsfetch, swallowingSet-Cookieand the redirect statusEnvironment
kimaki tunnel(traforo 0.7.2)Bug 1 — WS frames dropped during upstream connect
In
handleWsOpen(src/client.ts:431),localWsis registered inthis.localWsConnectionsonly inside the'open'callback. Anyws_framemessage that arrives before that callback runs hitshandleWsFrame(src/client.ts:513), which does:and drops the frame. Phoenix LiveView sends
phx_joinimmediately after the WebSocket opens, so that first frame is always the victim. The browser then loopsWS OPEN→ frame dropped → timeout → reconnect forever.Repro (100% deterministic): raw WebSocket to
wss://<tunnel>/live/websocket?vsn=2.0.0, send a frame inonopen:WS FRAME for unknown connection, no reply)Fix: buffer frames in
handleWsFramewhile the connection is pending, flush them inside the'open'callback after registeringlocalWs, and clear the buffer onerror/close.Bug 2 — Cookie header stripped from WS upgrade
handleWsOpen(src/client.ts:431-447) only forwards theSec-WebSocket-Protocolsubprotocol:The client's
Cookieheader is never forwarded to the upstream WebSocket. Phoenix LiveView requires the session cookie to authorizephx_join— without it the server rejects with{:error, %{reason: "stale"}}.Repro: WS echo server that logs the request's
Cookieheader. Connect withCookie: phx_auth=SECRET-COOKIE-123:ws://localhost:PORT) → server seesCOOKIE: phx_auth=SECRET-COOKIE-123COOKIE: (none)Fix: forward
cookie(and ideallyorigin) headers through thewsclient options:Bug 3 — HTTP redirects followed internally
handleHttpRequest(src/client.ts:335) usesfetch(url, ...)with noredirectoption. Node'sfetchfollows 3xx redirects by default. A Phoenix login POST returns302 Found+Set-Cookie+Location: /; the tunnel'sfetchfollows that redirect internally, and because the redirect chain's cookies are never applied to the follow-up request, the browser only ever sees the final 200 login page — the session cookie and the 302 never reach the client.Repro: POST valid credentials to
/operators/log-inon a Phoenix app:http://localhost:4000) →302 Found+Set-Cookie+Location: /200(final login page), noSet-Cookie, noLocationFix: add
redirect: 'manual'to thefetchoptions so the 3xx + headers are returned to the client as-is.Impact
Phoenix LiveView apps (and any client that relies on immediate WS frames, WS session cookies, or 3xx redirects — Next.js auth flows, SSE endpoints, HMR) are unusable through traforo. These were found via a Phoenix LiveView app; all three repros are deterministic and scriptable.
I have a working patch for all three in the local
dist/client.jsand can open a PR if useful.