diff --git a/test/bin-store-ping.test.ts b/test/bin-store-ping.test.ts index 6882827..8457cb5 100644 --- a/test/bin-store-ping.test.ts +++ b/test/bin-store-ping.test.ts @@ -100,24 +100,40 @@ describe("mongo bin store ping", () => { } }); - it("keeps a second socket proxy error inside the process", () => { + 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(); 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.emit("error", new Error("TLS handshake failed")); + }).not.toThrow(); + expect(seen).toEqual([proxy]); + const later: unknown[] = []; socket.on("error", (error: unknown) => { - seen.push(error); + later.push(error); }); - socket.emit("error", proxy); - expect(seen).toEqual([proxy]); + 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 92b8d4b..6a519f8 100644 --- a/worker/bin-store.ts +++ b/worker/bin-store.ts @@ -15,13 +15,16 @@ 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; +const socketsWithError = new WeakSet(); /** - * 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 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) { @@ -37,22 +40,17 @@ export function installSocketProxyGuard(): void { event: string | symbol, ...args: unknown[] ) { - if ( - event === "error" && - this.listenerCount("error") === 0 && - isSocketProxyFailure(args[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); }; } -function isSocketProxyFailure(error: unknown): boolean { - const message = error instanceof Error ? error.message : String(error ?? ""); - return message.includes(SOCKET_PROXY_FAILURE); -} - installSocketProxyGuard(); const workerBinMongoOptions = {