From 32f32051d06e5a289dd75532b83bd8758daf09b9 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 5 Feb 2026 09:00:01 +0700 Subject: [PATCH] Fix relay client reconnect self-replacement --- packages/server/src/client/daemon-client.ts | 28 +++++++++++++-------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/server/src/client/daemon-client.ts b/packages/server/src/client/daemon-client.ts index db20ae0a9..7c8762cf9 100644 --- a/packages/server/src/client/daemon-client.ts +++ b/packages/server/src/client/daemon-client.ts @@ -337,7 +337,11 @@ export class DaemonClient { } try { - this.cleanupTransport(); + // If we reconnect while the previous socket is still open (common in browsers + // where `onerror` may fire before `onclose`), we can end up with multiple + // concurrent relay sockets. Cloudflare then closes the old one with + // "Replaced by new connection", causing a disconnect loop. + this.disposeTransport(); const baseTransportFactory = this.config.transportFactory ?? createWebSocketTransportFactory( @@ -465,15 +469,7 @@ export class DaemonClient { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = null; } - this.cleanupTransport(); - if (this.transport) { - try { - this.transport.close(); - } catch { - // no-op - } - this.transport = null; - } + this.disposeTransport(1000, "Client closed"); this.clearWaiters(new Error("Daemon client closed")); this.updateConnectionState({ status: "disconnected", @@ -2297,6 +2293,18 @@ export class DaemonClient { return requestId ?? crypto.randomUUID(); } + private disposeTransport(code = 1001, reason = "Reconnecting"): void { + this.cleanupTransport(); + if (this.transport) { + try { + this.transport.close(code, reason); + } catch { + // no-op + } + this.transport = null; + } + } + private cleanupTransport(): void { if (this.pendingGenericTransportErrorTimeout) { clearTimeout(this.pendingGenericTransportErrorTimeout);