Skip to content

Editor stuck at "Connecting…": collab-session advertises ws://…:4001 but WS is multiplexed on the main port #53

Description

@mmoren10

Summary

On a default self-hosted npm run serve, the editor loads but hangs at "Connecting…". GET /documents/:slug/collab-session advertises a collab websocket URL on appPort + 1 (e.g. ws://localhost:4001/ws), but the WebSocket server is attached to the main HTTP port (:4000). Nothing listens on :4001, so the client retries forever.

Environment

  • Self-hosted, npm run serve on :4000, no COLLAB_* env vars set
  • Version: fb25787

Root cause — embedded vs. split-port mismatch

  • server/index.ts:42 attaches the WS server to the main HTTP server at path /ws:
    const wss = new WebSocketServer({ server, path: '/ws' });
  • server/index.ts:132 starts the collab runtime embedded (multiplexed on the main port):
    await startCollabRuntimeEmbedded(PORT);
    A direct handshake to ws://localhost:4000/ws?slug=<slug>&token=<accessToken> succeeds, confirming the WS lives on :4000.
  • But server/routes.ts:579 resolveRequestScopedCollabWsBase(req) decides the client-facing WS port from the COLLAB_EMBEDDED_WS env flag (routes.ts:589). When it is unset, the code assumes a split-port deployment and returns appPort + 1 (routes.ts:613):
    wsUrl.port = String(appPort + 1);
  • So collab-session returns collabWsUrl: ws://localhost:4001/ws, which is dead.

The defaults are inverted: the server attaches WS to the main port by default (startCollabRuntimeEmbedded), but the URL resolver defaults to split-port.

Reproduction

npm run serve   # no COLLAB_* env
# create a doc, then:
curl -s 'http://localhost:4000/documents/<slug>/collab-session?token=<accessToken>' | jq .session.collabWsUrl
# -> "ws://localhost:4001/ws?slug=<slug>"
lsof -iTCP:4001 -sTCP:LISTEN   # -> nothing

Editor → "Connecting…" indefinitely.

Workaround

Set one of:

  • COLLAB_EMBEDDED_WS=true (keeps the WS on the main port — routes.ts:605-607), or
  • COLLAB_PUBLIC_BASE_URL=ws://localhost:4000/ws

Suggested fix

Derive embedded-vs-split-port from the runtime that was actually started (the collab runtime already knows its wsUrlBase and whether it's attached/embedded) rather than from the COLLAB_EMBEDDED_WS env flag — or make startCollabRuntimeEmbedded set the default so resolveRequestScopedCollabWsBase keeps the same port unless explicitly told otherwise.

Found while debugging a self-hosted instance with Claude Code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions