From bef24dc4b028e1eac7ecdfa24f18901430fa833f Mon Sep 17 00:00:00 2001 From: Richard A Date: Thu, 29 Jan 2026 20:57:32 +0400 Subject: [PATCH] fix(gateway): install unhandled rejection handler in macOS daemon (#3815) The gateway daemon entrypoint did not call installUnhandledRejectionHandler(), causing transient fetch/network errors to crash the process. The CLI and relay paths already install it; this adds the same protection to the daemon path. Closes #3815 --- CHANGELOG.md | 1 + src/macos/gateway-daemon.test.ts | 59 ++++++++++++++++++++++++++++++++ src/macos/gateway-daemon.ts | 4 +++ 3 files changed, 64 insertions(+) create mode 100644 src/macos/gateway-daemon.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b6e8445bc..45b527d16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Status: beta. - Memory Search: allow extra paths for memory indexing. (#3600) Thanks @kira-ariaki. ### Changes +- Gateway: prevent crash on transient fetch/network failures (#3815) - Providers: add Venice AI integration; update Moonshot Kimi references to kimi-k2.5; update MiniMax API endpoint/format. (#2762, #3064) - Telegram: quote replies, edit-message action, silent sends, sticker support + vision caching, linkPreview toggle, plugin sendPayload support. (#2900, #2394, #2382, #2548, #1700, #1917) - Discord: configurable privileged gateway intents for presences/members. (#2266) Thanks @kentaro. diff --git a/src/macos/gateway-daemon.test.ts b/src/macos/gateway-daemon.test.ts new file mode 100644 index 000000000..7208ec7c4 --- /dev/null +++ b/src/macos/gateway-daemon.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import process from "node:process"; + +describe("gateway-daemon unhandled rejection handler", () => { + let exitCalls: Array = []; + let consoleErrorSpy: ReturnType; + let consoleWarnSpy: ReturnType; + + beforeEach(async () => { + exitCalls = []; + + vi.spyOn(process, "exit").mockImplementation((code: string | number | null | undefined) => { + if (code !== undefined && code !== null) { + exitCalls.push(code); + } + }); + + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + // Install the handler (same import the daemon uses) + const { installUnhandledRejectionHandler } = await import( + "../infra/unhandled-rejections.js" + ); + installUnhandledRejectionHandler(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("does NOT exit on transient fetch failures (ECONNRESET)", () => { + const fetchErr = Object.assign(new TypeError("fetch failed"), { + cause: { code: "ECONNRESET" }, + }); + + process.emit("unhandledRejection", fetchErr, Promise.resolve()); + + expect(exitCalls).toEqual([]); + expect(consoleWarnSpy).toHaveBeenCalledWith( + "[moltbot] Non-fatal unhandled rejection (continuing):", + expect.stringContaining("fetch failed"), + ); + }); + + it("exits on fatal errors like OOM", () => { + const oomErr = Object.assign(new Error("Out of memory"), { + code: "ERR_OUT_OF_MEMORY", + }); + + process.emit("unhandledRejection", oomErr, Promise.resolve()); + + expect(exitCalls).toEqual([1]); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "[moltbot] FATAL unhandled rejection:", + expect.stringContaining("Out of memory"), + ); + }); +}); diff --git a/src/macos/gateway-daemon.ts b/src/macos/gateway-daemon.ts index 686f705c2..50ebdb58d 100644 --- a/src/macos/gateway-daemon.ts +++ b/src/macos/gateway-daemon.ts @@ -41,6 +41,10 @@ async function main() { (globalThis as unknown as { Long?: unknown }).Long = Long; } + // Prevent crashes on transient fetch/network failures (#3815) + const { installUnhandledRejectionHandler } = await import("../infra/unhandled-rejections.js"); + installUnhandledRejectionHandler(); + const [ { loadConfig }, { startGatewayServer },