From 5dfe2eacc9a50c4ef13f621ae67bab273294db04 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 27 Jan 2026 22:13:25 +0000 Subject: [PATCH] Add gateway.unhandledRejections config (warn|exit) --- PR_UNHANDLED_REJECTIONS.md | 28 ++++++++++++++++++++++++++++ src/cli/run-main.ts | 7 +++++-- src/config/types.gateway.ts | 8 ++++++++ src/config/zod-schema.ts | 1 + src/index.ts | 7 +++++-- src/infra/unhandled-rejections.ts | 7 ++++++- 6 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 PR_UNHANDLED_REJECTIONS.md diff --git a/PR_UNHANDLED_REJECTIONS.md b/PR_UNHANDLED_REJECTIONS.md new file mode 100644 index 000000000..bdcbc983c --- /dev/null +++ b/PR_UNHANDLED_REJECTIONS.md @@ -0,0 +1,28 @@ +# Proposal: Configurable gateway unhandled promise rejection policy + +## Motivation +Running Clawdbot Gateway in production-like environments can encounter transient network errors (undici `fetch failed`, DNS hiccups, Telegram API blips). Today, any *unhandled* promise rejection can terminate the gateway process (`process.exit(1)`), causing user-visible downtime and missed replies. + +Diego’s deployment uses systemd with `Restart=always`, but the restart still interrupts message handling. + +## Desired behavior +Add a JSON config knob to control what happens on unhandled promise rejections: + +```json5 +{ + gateway: { + unhandledRejections: "warn" // or "exit" + } +} +``` + +- `exit` (default): keep current behavior for safety; exit 1 so supervisors restart. +- `warn`: never exit the gateway for unhandled rejections; log as error/warn and continue. + +## Notes +- Existing suppression for AbortError / transient network errors should remain regardless of mode. +- In `warn` mode, non-network/unexpected unhandled rejections should still be logged loudly. + +## Acceptance criteria +- With `gateway.unhandledRejections="warn"`, a forced Telegram DNS failure should result in a logged error but the gateway process should remain running. +- With `exit` (default), behavior remains unchanged. diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index c3a9ae466..4ebbcea8a 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -10,6 +10,7 @@ import { ensureMoltbotCliOnPath } from "../infra/path-env.js"; import { assertSupportedRuntime } from "../infra/runtime-guard.js"; import { formatUncaughtError } from "../infra/errors.js"; import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js"; +import { loadConfig } from "../config/config.js"; import { enableConsoleCapture } from "../logging.js"; import { getPrimaryCommand, hasHelpOrVersion } from "./argv.js"; import { tryRouteCli } from "./route.js"; @@ -41,8 +42,10 @@ export async function runCli(argv: string[] = process.argv) { const program = buildProgram(); // Global error handlers to prevent silent crashes from unhandled rejections/exceptions. - // These log the error and exit gracefully instead of crashing without trace. - installUnhandledRejectionHandler(); + // Default behavior is "exit" (preserves historical behavior). Users can opt into + // "warn" via config: gateway.unhandledRejections. + const cfg = loadConfig(); + installUnhandledRejectionHandler({ mode: cfg.gateway?.unhandledRejections ?? "exit" }); process.on("uncaughtException", (error) => { console.error("[moltbot] Uncaught exception:", formatUncaughtError(error)); diff --git a/src/config/types.gateway.ts b/src/config/types.gateway.ts index a0d562f7b..e9ff8b0fe 100644 --- a/src/config/types.gateway.ts +++ b/src/config/types.gateway.ts @@ -205,9 +205,17 @@ export type GatewayNodesConfig = { denyCommands?: string[]; }; +export type GatewayUnhandledRejectionsMode = "exit" | "warn"; + export type GatewayConfig = { /** Single multiplexed port for Gateway WS + HTTP (default: 18789). */ port?: number; + /** + * What to do on unhandled promise rejections in the gateway process. + * - exit (default): log + exit(1) + * - warn: log but keep running + */ + unhandledRejections?: GatewayUnhandledRejectionsMode; /** * Explicit gateway mode. When set to "remote", local gateway start is disabled. * When set to "local", the CLI may start the gateway locally. diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index ce4115517..38bf28f38 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -304,6 +304,7 @@ export const MoltbotSchema = z .object({ port: z.number().int().positive().optional(), mode: z.union([z.literal("local"), z.literal("remote")]).optional(), + unhandledRejections: z.union([z.literal("exit"), z.literal("warn")]).optional(), bind: z .union([ z.literal("auto"), diff --git a/src/index.ts b/src/index.ts index 6a02a828d..b99079d76 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,7 @@ import { import { assertSupportedRuntime } from "./infra/runtime-guard.js"; import { formatUncaughtError } from "./infra/errors.js"; import { installUnhandledRejectionHandler } from "./infra/unhandled-rejections.js"; +import { loadConfig } from "./config/config.js"; import { enableConsoleCapture } from "./logging.js"; import { runCommandWithTimeout, runExec } from "./process/exec.js"; import { assertWebChannel, normalizeE164, toWhatsappJid } from "./utils.js"; @@ -79,8 +80,10 @@ const isMain = isMainModule({ if (isMain) { // Global error handlers to prevent silent crashes from unhandled rejections/exceptions. - // These log the error and exit gracefully instead of crashing without trace. - installUnhandledRejectionHandler(); + // Default behavior is "exit" (preserves historical behavior). Users can opt into + // "warn" via config: gateway.unhandledRejections. + const cfg = loadConfig(); + installUnhandledRejectionHandler({ mode: cfg.gateway?.unhandledRejections ?? "exit" }); process.on("uncaughtException", (error) => { console.error("[moltbot] Uncaught exception:", formatUncaughtError(error)); diff --git a/src/infra/unhandled-rejections.ts b/src/infra/unhandled-rejections.ts index 108b6c016..757ad1cb9 100644 --- a/src/infra/unhandled-rejections.ts +++ b/src/infra/unhandled-rejections.ts @@ -104,7 +104,11 @@ export function isUnhandledRejectionHandled(reason: unknown): boolean { return false; } -export function installUnhandledRejectionHandler(): void { +export type UnhandledRejectionsMode = "exit" | "warn"; + +export function installUnhandledRejectionHandler(opts: { mode?: UnhandledRejectionsMode } = {}): void { + const mode: UnhandledRejectionsMode = opts.mode ?? "exit"; + process.on("unhandledRejection", (reason, _promise) => { if (isUnhandledRejectionHandled(reason)) return; @@ -123,6 +127,7 @@ export function installUnhandledRejectionHandler(): void { } console.error("[moltbot] Unhandled promise rejection:", formatUncaughtError(reason)); + if (mode === "warn") return; process.exit(1); }); }