From c0f087a9102ef325eab146832055da7ec2ee3220 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 17 May 2026 23:50:53 -0700 Subject: handle socket-level errors that bypass Bun's JS error path Two related defenses around the CLN unix socket connection: - Global net.Socket.prototype.connect patch: when Bun's net.createConnection is called on a non-existent unix socket, the ENOENT it throws bypasses all JS error handlers and crashes the process. existsSync-check the path first; if missing, emit a normal 'error' event so the caller can handle it gracefully. - lightningProxy: existsSync gate before constructing LightningClient, plus a client.on("error") listener that clears the cached client on socket-died errors instead of letting the unhandled event take down the process. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/ln.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/lib/ln.ts b/lib/ln.ts index 6b94da16..2cbd5120 100644 --- a/lib/ln.ts +++ b/lib/ln.ts @@ -1,4 +1,30 @@ import config from "$config"; +import { existsSync } from "fs"; +import net from "net"; + +// Patch net.Socket.prototype.connect to prevent Bun from crashing when +// connecting to a non-existent unix socket. Bun's net.createConnection +// throws ENOENT in a way that bypasses all JS error handlers, taking the +// whole process down. Intercept early and emit a proper error event. +const origConnect = net.Socket.prototype.connect; +net.Socket.prototype.connect = function (...args: any[]) { + const sockPath = typeof args[0] === "string" ? args[0] : args[0]?.path; + if (typeof sockPath === "string" && sockPath.startsWith("/") && !existsSync(sockPath)) { + process.nextTick(() => { + this.emit( + "error", + Object.assign(new Error(`connect ENOENT ${sockPath}`), { + code: "ENOENT", + errno: -2, + syscall: "connect", + address: sockPath, + }), + ); + }); + return this; + } + return origConnect.apply(this, args); +}; const mod = (await import("@asoltys/clightning-client")).default; const { LightningClient } = mod as { LightningClient: any }; @@ -36,8 +62,20 @@ function lightningProxy(rpcPath: string): any { throw new LightningUnavailableError(`Lightning not ready at ${rpcPath}`); } + if (!existsSync(rpcPath)) { + nextTryAt = Date.now() + backoff; + backoff = Math.min(maxBackoff, Math.floor(backoff * 1.8)); + throw new LightningUnavailableError( + `Lightning RPC socket not found at ${rpcPath}`, + ); + } + try { client = new LightningClient(rpcPath); + // Prevent unhandled 'error' events from crashing the process + client.on("error", (e: any) => { + if (isSocketDied(e)) client = null; + }); backoff = 250; nextTryAt = 0; return client; -- cgit v1.2.3