From 0639c7bf1f37bafeb847afc9e422f05f3bb084a3 Mon Sep 17 00:00:00 2001 From: spiceoogway Date: Fri, 30 Jan 2026 04:15:03 -0500 Subject: [PATCH] Fix #4501: Handle unhandled promise rejections in Telegram polling - Add global unhandled rejection handler in gateway startup - Properly await runner.stop() in Telegram monitor to prevent unhandled rejections - Ensure runner cleanup in finally block to prevent lingering promises - Log rejections instead of crashing the gateway Fixes issue where Telegram Bot API network failures during idle polling would cause unhandled promise rejections and crash the gateway process. The fix adds defensive error handling at both the gateway level (global handler) and the Telegram provider level (proper promise cleanup). --- src/gateway/server.impl.ts | 16 ++++++++++++++++ src/telegram/monitor.ts | 14 +++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index efa91be76..a793966b6 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -148,6 +148,19 @@ export async function startGatewayServer( port = 18789, opts: GatewayServerOptions = {}, ): Promise { + // Install global unhandled rejection handler to prevent gateway crashes + // from background promises (e.g., Telegram polling, network operations) + const handleUnhandledRejection = (reason: unknown, promise: Promise) => { + const formatted = reason instanceof Error ? reason.message : String(reason); + log.error(`unhandled promise rejection: ${formatted}`); + // Log additional details for debugging + if (reason instanceof Error && reason.stack) { + log.debug(`rejection stack: ${reason.stack}`); + } + // Don't crash the gateway - log and continue + }; + process.on("unhandledRejection", handleUnhandledRejection); + // Ensure all default port derivations (browser/canvas) see the actual runtime port. process.env.OPENCLAW_GATEWAY_PORT = String(port); logAcceptedEnvOption({ @@ -572,6 +585,9 @@ export async function startGatewayServer( return { close: async (opts) => { + // Remove unhandled rejection handler on shutdown + process.off("unhandledRejection", handleUnhandledRejection); + if (diagnosticsEnabled) { stopDiagnosticHeartbeat(); } diff --git a/src/telegram/monitor.ts b/src/telegram/monitor.ts index 2709b591b..17ea74a18 100644 --- a/src/telegram/monitor.ts +++ b/src/telegram/monitor.ts @@ -161,7 +161,12 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) { const runner = run(bot, createTelegramRunnerOptions(cfg)); const stopOnAbort = () => { if (opts.abortSignal?.aborted) { - void runner.stop(); + // Properly await runner.stop() to prevent unhandled rejections + runner.stop().catch((err) => { + (opts.runtime?.error ?? console.error)( + `telegram: runner stop failed: ${formatErrorMessage(err)}`, + ); + }); } }; opts.abortSignal?.addEventListener("abort", stopOnAbort, { once: true }); @@ -194,6 +199,13 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) { } } finally { opts.abortSignal?.removeEventListener("abort", stopOnAbort); + // Ensure runner is stopped to prevent lingering promises + try { + await runner.stop(); + } catch (stopErr) { + // Suppress errors from runner.stop() in finally block + // (already logged by stopOnAbort if abort-triggered) + } } } }