aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-05-17 23:50:53 -0700
committerAdam Soltys <[email protected]>2026-05-17 23:50:53 -0700
commitc0f087a9102ef325eab146832055da7ec2ee3220 (patch)
tree967995b4f36c86d83f335570f375902b9272c5dc
parent7c1eefc142c951d7db5a0fa1afeae64a2fdf02fd (diff)
downloadcoinos-server-c0f087a9102ef325eab146832055da7ec2ee3220.tar.gz
coinos-server-c0f087a9102ef325eab146832055da7ec2ee3220.zip
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) <[email protected]>
-rw-r--r--lib/ln.ts38
1 files changed, 38 insertions, 0 deletions
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;