Skip to content

Open the app's sockets against the server, so no proxy has to carry them - #421

Merged
davidmckayv merged 4 commits into
CopilotKit:mainfrom
zopeVaibhav:fix/sockets-bypass-vite-proxy
Sep 8, 2026
Merged

Open the app's sockets against the server, so no proxy has to carry them#421
davidmckayv merged 4 commits into
CopilotKit:mainfrom
zopeVaibhav:fix/sockets-bypass-vite-proxy

Conversation

@zopeVaibhav

Copy link
Copy Markdown
Contributor

What this changes

The app built both its WebSocket URLs from its own address, so both travelled through Vite's /api proxy. Under bun that proxy cannot carry a WebSocket (oven-sh/bun#24127, a regression since bun 1.3.1 whose fix is unmerged), so neither the Bot's screen nor live channel updates ever connected, and an upgrade the server answered with plain HTTP crashed the process serving the app.

Both now address the server directly. app/src/lib/socket-url.ts keeps the host the person actually used and changes only the port, so reaching the app at 192.168.1.10:3010 opens its socket at 192.168.1.10:3001 rather than somebody else's loopback. When no server port is configured it returns the page's own origin, so a deployment serving app and API from one origin is unchanged — pinned by a test.

With nothing upgrading through the proxy, ws: true is no longer carrying anything, so it is gone. That is what removes the crash rather than a workaround for it: the branch containing socket.destroySoon() is only reached when the proxy is asked to handle an upgrade.

__OPENBOT_SERVER_PORT__ is defined in vite.config.ts from SERVER_PORT, which is already in the environment Vite is started with, in dev and in the serve script the desktop shell runs.

Fixes #420.

Where it runs

This is browser code choosing an address, plus one build-time constant.

  • New state that outlives a request? None.
  • What happens on the second replica? Nothing changes. The socket address is derived per browser from the page's own hostname; no process holds anything. A hosted deployment serving app and API from one origin configures no port and keeps today's behaviour exactly.
  • Anything serialised? No.
  • Anything fanned out to a browser? The two existing sockets, unchanged in protocol and payload. Only the address they dial moved.
  • New listener, port, or schedule? None. The server already listened on this port and already served both socket routes.

Boundary and audit

  • Every acting call still goes through the gateway: unchanged, no server code touched.
  • New refusals and failures each write a row: no new refusal or failure exists.
  • Nothing new is trusted from the client: the socket address is derived from the page's own location plus a build-time constant, never from anything a request carries. The server's own guard on /api/computers/<bot>/streamresolveRequestActor, applied by hand because middleware does not run on an upgrade — is untouched and still runs.

Changelog

  • A line in CHANGELOG.md under Unreleased.

Proof

Measured, not argued. The upstream cause, with a control that isolates the runtime:

vite under node v24     server direct: OPEN    through vite: OPEN
vite under bun 1.3.14   server direct: OPEN    through vite: TIMEOUT

Before and after, in a browser on the running app:

before   ws://localhost:3010/api/channels/events  TIMEOUT,  405 console errors on one page load
after    ws://localhost:3001/api/channels/events  OPEN,     0 console errors

The live screen, on the endpoint the component now builds, against a running computer:

ws://localhost:3001/api/computers/general-assistant/stream
opened: true, first frame 8432 bytes

The crash, on both paths that serve the app, with the server up so the upgrade gets a real non-101 answer. Before, one request took both ports down and logged the destroySoon TypeError. After:

bun run dev      curl upgrade /api/agents                    server=200 app=200
bun run dev      curl upgrade /api/computers/x/stream (503)  server=200 app=200
vite preview     curl upgrade /api/agents                    app=200
vite preview     curl upgrade /api/computers/x/stream (503)  app=200
destroySoon occurrences in either log: 0

The one assumption worth stating was tested rather than reasoned about. Moving a socket to another port only works if the session cookie follows it, so a cookie was set on the page at :3010 and the server on :3001 was instrumented to log what arrived on the handshake:

path: /api/channels/events   upgrade: websocket
origin: http://localhost:3010
cookie: openbot_probe_session=abc123

Cookies ignore ports and the two are same-site, so the session reaches the guard exactly as before. The instrumentation was reverted; it is not in this branch.

App tests: 348 pass / 0 fail on main, 352 pass / 0 fail here, the four new ones covering the address chosen. bun run typecheck clean across app, server and worker; biome lint --error-on-warnings clean over 575 files. The repository suite's handoff-queue lease tests fail on this branch and on a clean main alike, and their count moves between runs on main too — they are not touched here.

Not covered: this was verified on macOS with bun 1.3.14 and vite 7.3.6, and against a deployment with no identity provider configured, so the cookie above was a stand-in for a real session rather than one.

@zopeVaibhav
zopeVaibhav force-pushed the fix/sockets-bypass-vite-proxy branch from 644f6bb to 4a1a290 Compare September 8, 2026 19:32

@davidmckayv davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep-reviewed against live code (correctness, governance, no vendor/secret/scale issues). Composed build+tests green. CHANGELOG/format rebase on CI-validated substance.

@davidmckayv
davidmckayv merged commit 8817249 into CopilotKit:main Sep 8, 2026
14 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.

Under bun the app's /api WebSocket proxy never connects, and a refused upgrade ends the process

2 participants