Skip to content

fix(lan-share): carry app WebSockets, only divert Vite's HMR socket#657

Merged
geodro merged 1 commit into
lerd-env:mainfrom
prabowosd:fix/lan-share-websocket-upgrades
Jun 27, 2026
Merged

fix(lan-share): carry app WebSockets, only divert Vite's HMR socket#657
geodro merged 1 commit into
lerd-env:mainfrom
prabowosd:fix/lan-share-websocket-upgrades

Conversation

@prabowosd

Copy link
Copy Markdown
Member

Problem

lerd lan:share forwarded plain HTTP fine but dropped WebSocket upgrades for LAN clients: a WS handshake hung (curl http_code=000, full timeout) while the identical request straight to the site's nginx vhost returned 101 Switching Protocols. Reverb's /app/<key> and a noVNC /websockify endpoint both reproduced it (#656).

Root cause

The share handler diverted every WebSocket upgrade to the active Vite dev server whenever one was known:

if isWebSocketUpgrade(r) {
    if port := h.getActiveVitePort(); port > 0 {
        h.viteProxy(port).ServeHTTP(w, r)
        return
    }
}

During a normal Laravel dev session (npm run dev running for HMR), activeVitePort is set, so application sockets were hijacked to Vite. Vite does not speak Reverb's or noVNC's protocol, so the handshake hangs. The main proxy path (httputil.ReverseProxy to nginx) carries WebSockets natively and was never the problem, it just never got the chance to run.

Fix

Only Vite's HMR client connects to the bare origin (/?token=...). Gate the diversion on that, so application sockets (which carry a real path) fall through to the main proxy and get upgraded by nginx exactly like its own vhost.

if isWebSocketUpgrade(r) && isViteHMRSocket(r) { /* divert to Vite */ }

Verification

Added an end-to-end test (lanshare_ws_e2e_test.go) that runs the real lanShareHandler + httputil.ReverseProxy and performs a raw WS handshake:

  • no Vite active: app WS reaches the main proxy and upgrades (101), confirming that path was always WS-capable.
  • Vite active: /app/<key> and /websockify still reach the main proxy and upgrade, while Vite's HMR socket (/) still routes to Vite.

The Vite-active test fails on main (the app socket is answered by the Vite stand-in) and passes with this change. Full internal/cli lan-share suite green; go vet clean.

Note / out of scope

The secured share builds its upstream transport from http.DefaultTransport.Clone() (ForceAttemptHTTP2=true). This is safe today because lerd's nginx never enables HTTP/2, so ALPN always settles on HTTP/1.1 and the h1 WS upgrade passes through. If HTTP/2 is ever enabled on a vhost (e.g. via a custom.d include), the secured share could negotiate h2 and break the upgrade. Pinning the secured transport to http/1.1 would harden against that, but it is not needed for #656 and is left out here.

Fixes #656.

The LAN share proxy diverted every WebSocket upgrade to the active Vite
dev server whenever one was known. During a normal Laravel dev session
(npm run dev running for HMR), that hijacked application sockets too:
Reverb's /app/<key> and noVNC's /websockify went to Vite, which does not
speak their protocol, so the handshake hung (curl http_code=000) while
plain HTTP pages kept loading.

Only Vite's HMR client connects to the bare origin ("/?token=..."), so
gate the diversion on that path. Application sockets carry a real path
and now fall through to the main proxy, where httputil.ReverseProxy
upgrades them natively, exactly like the site's own nginx vhost.

Fixes lerd-env#656.
@geodro geodro merged commit 2fed5d7 into lerd-env:main Jun 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lan:share doesn’t proxy WebSocket upgrades

2 participants