From 751328b7c6a5d2a9ce6ae0bd1cb303aa63374015 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 03:03:02 +0000 Subject: [PATCH 1/2] fix: drop every second socket error with no listener workerd emits a socket error twice. The message is not always the proxy failure. A TLS failure or a reset can abort the isolate the same way. Co-authored-by: Jared Wray --- test/bin-store-ping.test.ts | 16 ++++++++-------- worker/bin-store.ts | 20 ++++++-------------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/test/bin-store-ping.test.ts b/test/bin-store-ping.test.ts index 6882827..32a3fec 100644 --- a/test/bin-store-ping.test.ts +++ b/test/bin-store-ping.test.ts @@ -100,23 +100,23 @@ describe("mongo bin store ping", () => { } }); - it("keeps a second socket proxy error inside the process", () => { + it("drops a second socket error that has no listener", () => { installSocketProxyGuard(); const socket = new net.Socket(); const proxy = new Error( "proxy request failed, cannot connect to the specified address", ); + const seen: unknown[] = []; + socket.once("error", (error: unknown) => { + seen.push(error); + }); + expect(socket.emit("error", proxy)).toBe(true); expect(() => { socket.emit("error", proxy); }).not.toThrow(); expect(() => { - socket.emit("error", new Error("other failure")); - }).toThrow(/other failure/); - const seen: unknown[] = []; - socket.on("error", (error: unknown) => { - seen.push(error); - }); - socket.emit("error", proxy); + socket.emit("error", new Error("TLS handshake failed")); + }).not.toThrow(); expect(seen).toEqual([proxy]); socket.destroy(); }); diff --git a/worker/bin-store.ts b/worker/bin-store.ts index 92b8d4b..5530c19 100644 --- a/worker/bin-store.ts +++ b/worker/bin-store.ts @@ -15,13 +15,14 @@ import type { MockHttpBinsOptions } from "../src/mock-http-bins.js"; export const BIN_STORE_PING_TIMEOUT_MS = 2_000; -const SOCKET_PROXY_FAILURE = "proxy request failed"; let socketProxyGuardInstalled = false; /** - * workerd reports a refused socket twice. The driver handles the first emit. - * The second emit has no listener, so the isolate aborts. - * Keep that second emit inside the process. + * workerd `node:net` emits `error` twice for one failed socket. + * `cleanupAfterDestroy` emits the first time. The stream `destroy` callback emits again. + * The driver listens with `once`, so the second emit has no listener and the isolate aborts. + * The message differs for a proxy failure, a TLS failure, or a reset. + * Drop each `error` that has no listener. `tls.TLSSocket` uses this `emit`. */ export function installSocketProxyGuard(): void { if (socketProxyGuardInstalled) { @@ -37,22 +38,13 @@ export function installSocketProxyGuard(): void { event: string | symbol, ...args: unknown[] ) { - if ( - event === "error" && - this.listenerCount("error") === 0 && - isSocketProxyFailure(args[0]) - ) { + if (event === "error" && this.listenerCount("error") === 0) { return false; } return emit.call(this, event, ...args); }; } -function isSocketProxyFailure(error: unknown): boolean { - const message = error instanceof Error ? error.message : String(error ?? ""); - return message.includes(SOCKET_PROXY_FAILURE); -} - installSocketProxyGuard(); const workerBinMongoOptions = { From cecd702926afb83b8129ffb7e504a9f1f3bdc9ac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 20 Oct 2026 03:09:08 +0000 Subject: [PATCH 2/2] fix: throw the first socket error that has no listener The guard now records a socket after its first error. A later error with no listener is dropped. The first error still throws. Co-authored-by: Jared Wray --- test/bin-store-ping.test.ts | 16 ++++++++++++++++ worker/bin-store.ts | 12 +++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/test/bin-store-ping.test.ts b/test/bin-store-ping.test.ts index 32a3fec..8457cb5 100644 --- a/test/bin-store-ping.test.ts +++ b/test/bin-store-ping.test.ts @@ -100,6 +100,15 @@ describe("mongo bin store ping", () => { } }); + it("still throws the first socket error that has no listener", () => { + installSocketProxyGuard(); + const socket = new net.Socket(); + expect(() => { + socket.emit("error", new Error("first failure")); + }).toThrow(/first failure/); + socket.destroy(); + }); + it("drops a second socket error that has no listener", () => { installSocketProxyGuard(); const socket = new net.Socket(); @@ -118,6 +127,13 @@ describe("mongo bin store ping", () => { socket.emit("error", new Error("TLS handshake failed")); }).not.toThrow(); expect(seen).toEqual([proxy]); + const later: unknown[] = []; + socket.on("error", (error: unknown) => { + later.push(error); + }); + const again = new Error("later failure"); + expect(socket.emit("error", again)).toBe(true); + expect(later).toEqual([again]); socket.destroy(); }); }); diff --git a/worker/bin-store.ts b/worker/bin-store.ts index 5530c19..6a519f8 100644 --- a/worker/bin-store.ts +++ b/worker/bin-store.ts @@ -16,13 +16,15 @@ import type { MockHttpBinsOptions } from "../src/mock-http-bins.js"; export const BIN_STORE_PING_TIMEOUT_MS = 2_000; let socketProxyGuardInstalled = false; +const socketsWithError = new WeakSet(); /** * workerd `node:net` emits `error` twice for one failed socket. * `cleanupAfterDestroy` emits the first time. The stream `destroy` callback emits again. * The driver listens with `once`, so the second emit has no listener and the isolate aborts. * The message differs for a proxy failure, a TLS failure, or a reset. - * Drop each `error` that has no listener. `tls.TLSSocket` uses this `emit`. + * Drop a later `error` that has no listener. The first `error` still uses Node behavior. + * `tls.TLSSocket` uses this `emit`. */ export function installSocketProxyGuard(): void { if (socketProxyGuardInstalled) { @@ -38,8 +40,12 @@ export function installSocketProxyGuard(): void { event: string | symbol, ...args: unknown[] ) { - if (event === "error" && this.listenerCount("error") === 0) { - return false; + if (event === "error") { + const repeat = socketsWithError.has(this); + socketsWithError.add(this); + if (repeat && this.listenerCount("error") === 0) { + return false; + } } return emit.call(this, event, ...args); };