#1745·SurfSense

[BUG] /zero/* WebSocket connections drop ~30s in, Documents panel never loads (self-hosted)

Author: nidzhat666Created Sep 5, 2026Updated Sep 5, 2026

Bug Description

Every /zero/* WebSocket connection is closed abruptly by zero-cache roughly 30s after the handshake, in an infinite reconnect loop. The immediate effect is that any UI backed by Zero (Documents list, notifications, etc.) never finishes loading and stays stuck on its skeleton state, even though the REST API works fine and documents are indexed successfully in the background.

Deployment Type

  • SurfSense Cloud (hosted version)
  • Self-hosted version

Steps to Reproduce

This reproduces with zero application state — no login, no cookies, no real client — so it's not specific to any account/workspace/document:

bash
curl -s -i -N --http1.1 --max-time 40 \
  -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  "https://<your-domain>/zero/sync/v51/connect?clientID=test&clientGroupID=test&userID=test&baseCookie=&ts=0&lmid=0&wsid=test"

Response: 101 Switching Protocols, then the connection sits open for exactly ~30 seconds and delivers this payload before the socket dies:

Error: Unexpected undefined value

In the real app: log in, open the Documents panel — it shows the skeleton loading state and never resolves. DevTools console shows a WebSocket reconnect loop against /zero/sync/v51/connect with ClientError: WebSocket connection closed abruptly, repeating with increasing backoff forever.

Expected Behavior

The /zero/* WebSocket connection stays open and the Documents panel (and other Zero-synced views) load normally.

Actual Behavior

zero-cache closes every connection ~30s in. Debug-level logs (ZERO_LOG_LEVEL=debug) show the actual failure happens almost immediately after initConnection, not at the 30s mark — the 30s is just ws's default closeTimeout (node_modules/ws/lib/constants.js: CLOSE_TIMEOUT = 30000) elapsing while it waits for a clean close handshake that never happens, before hard-terminating the socket (hence code: 1006, wasClean: false on the client side).

Relevant debug trace (single connection, clientID=onavlbmb2jcbfahafk):

{"level":"DEBUG",...,"message":"acquired lock for cvr"}
{"level":"DEBUG",...,"cmd":"initConnection","message":"mismatched wsID  B-5e8huSLCsTscTm-sCL0"}
{"level":"DEBUG",...,"message":"No auth maintenance wakeup scheduled"}
{"level":"WARN","worker":"dispatcher","message":"closing connection to client with error Error: Unexpected undefined value"}
{"level":"INFO",...,"message":"closing clientGroupID=..."}
{"level":"WARN",...,"errorBody":{"kind":"Internal","message":"shut down before initialization completed","origin":"zeroCache"},"name":"ProtocolError",...}

Traced Error: Unexpected undefined value to shared/src/must.js (if (v == null) throw new Error(msg ?? \Unexpected ${v} value`)), and the close path to closeWithErrorinzero-cache/src/types/ws.js, called from the dispatcher's handoff error path in zero-cache/src/server/worker-dispatcher.js`. Wasn't able to pin the exact call site further without unminified source — happy to dig further if useful, or if there's a source-mapped build I can point at instead.

Note: the mismatched wsID line in view-syncer.js (client?.wsID !== wsID → debug-log + early return) looks like a benign, expected no-op on its own — flagging it only because it's the last view-syncer-side log line before the dispatcher's crash, in case it's a useful breadcrumb.

Things I ruled out (self-hosted repro, all reproduce identically)

  • Reverse proxy: replaced bundled Caddy with nginx-proxy-manager, replicating the exact /zero/context → backend, /zero/* → zero-cache routing from docker/proxy/Caddyfile. Bypassed the proxy entirely (curl straight to the zero-cache container on the Docker network) — identical ~30s failure, so this isn't a proxy/header issue.
  • ZERO_NUM_SYNC_WORKERS: reproduces at both 4 (default) and 1.
  • Zero-cache version: reproduces identically on 1.6.0 (the pin in docker/docker-compose.yml), 1.6.2, and latest (=1.9.0 as of this writing, confirmed via Docker Hub digest match) — not a regression that got fixed upstream.
  • Stale/desynced Postgres state: docker compose down -v (wiped postgres_data and every other named volume), fresh alembic upgrade head on an empty database, brand-new user registration — same failure from the very first connection attempt.

Environment Information

  • Browser: Chrome (latest)
  • Operating System: self-hosted server: Ubuntu 24.04 (Docker host); client OS irrelevant, reproduces via bare curl
  • SurfSense Version: ghcr.io/modsetter/surfsense-backend:latest / surfsense-web:latest (pulled 2026-09-04)

Additional Environment Details (for Self-hosted only)

  • Database: pgvector/pgvector:pg17, fresh install, wal_level=logical per bundled postgresql.conf
  • Deployment Method: Docker Compose, adapted from docker/docker-compose.yml — services/env otherwise unchanged (ETL_SERVICE=DOCLING, EMBEDDING_MODEL=litellm://text-embedding-3-small, AUTH_TYPE=LOCAL, SANDBOX_ENABLED=FALSE)
  • Only structural deviations from the upstream compose: nginx-proxy-manager instead of bundled Caddy (see above — proven not the cause), and opensandbox-server/sandbox-image omitted entirely (unrelated services, no dependency on zero-cache)

Additional Context

This blocks real usage, not just a console warning — the Documents panel (and presumably notifications, chat live-updates, and anything else backed by Zero per zero-sync.mdx) never leaves its loading skeleton, since it's sync-only with no REST fallback visible in the UI. Document upload/indexing via Celery and the REST API works fine in the background; it's specifically the Zero-synced read paths that never resolve.

Happy to run more diagnostics (e.g. capture the exact WS frames sent by the real browser client, or bisect zero-cache versions further) — this repro is fully scripted so turnaround is fast.

Checklist

  • I have searched existing issues to ensure this is not a duplicate
  • I have provided all the required information above
  • I have added appropriate labels (bug, deployment type)