Merge 5a22326ba4 into 4583f88626
This commit is contained in:
commit
4daf76b988
28
PR_UNHANDLED_REJECTIONS.md
Normal file
28
PR_UNHANDLED_REJECTIONS.md
Normal file
@ -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.
|
||||||
17
pr-body.md
Normal file
17
pr-body.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
## Summary
|
||||||
|
Add a JSON config knob to control unhandled promise rejection behavior in the Gateway/CLI.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
Transient network errors (e.g., undici `fetch failed`) can currently terminate the gateway when they surface as unhandled promise rejections. Some operators prefer warn-only behavior to avoid missed replies and restarts.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
- Add `gateway.unhandledRejections: "warn"|"exit"` to config types + zod schema.
|
||||||
|
- Wire config into `installUnhandledRejectionHandler({ mode })` in `src/index.ts` and `src/cli/run-main.ts`.
|
||||||
|
- Extend handler to accept `mode` and skip `process.exit(1)` when `mode="warn"`.
|
||||||
|
|
||||||
|
## Default behavior
|
||||||
|
Unchanged (defaults to `"exit"`).
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
- With `gateway.unhandledRejections="warn"`, unhandled rejections are logged but do not terminate the gateway.
|
||||||
|
- With default config, behavior remains unchanged.
|
||||||
@ -10,6 +10,7 @@ import { ensureMoltbotCliOnPath } from "../infra/path-env.js";
|
|||||||
import { assertSupportedRuntime } from "../infra/runtime-guard.js";
|
import { assertSupportedRuntime } from "../infra/runtime-guard.js";
|
||||||
import { formatUncaughtError } from "../infra/errors.js";
|
import { formatUncaughtError } from "../infra/errors.js";
|
||||||
import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
|
import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
|
||||||
|
import { loadConfig } from "../config/config.js";
|
||||||
import { enableConsoleCapture } from "../logging.js";
|
import { enableConsoleCapture } from "../logging.js";
|
||||||
import { getPrimaryCommand, hasHelpOrVersion } from "./argv.js";
|
import { getPrimaryCommand, hasHelpOrVersion } from "./argv.js";
|
||||||
import { tryRouteCli } from "./route.js";
|
import { tryRouteCli } from "./route.js";
|
||||||
@ -41,8 +42,10 @@ export async function runCli(argv: string[] = process.argv) {
|
|||||||
const program = buildProgram();
|
const program = buildProgram();
|
||||||
|
|
||||||
// Global error handlers to prevent silent crashes from unhandled rejections/exceptions.
|
// Global error handlers to prevent silent crashes from unhandled rejections/exceptions.
|
||||||
// These log the error and exit gracefully instead of crashing without trace.
|
// Default behavior is "exit" (preserves historical behavior). Users can opt into
|
||||||
installUnhandledRejectionHandler();
|
// "warn" via config: gateway.unhandledRejections.
|
||||||
|
const cfg = loadConfig();
|
||||||
|
installUnhandledRejectionHandler({ mode: cfg.gateway?.unhandledRejections ?? "exit" });
|
||||||
|
|
||||||
process.on("uncaughtException", (error) => {
|
process.on("uncaughtException", (error) => {
|
||||||
console.error("[moltbot] Uncaught exception:", formatUncaughtError(error));
|
console.error("[moltbot] Uncaught exception:", formatUncaughtError(error));
|
||||||
|
|||||||
@ -205,9 +205,17 @@ export type GatewayNodesConfig = {
|
|||||||
denyCommands?: string[];
|
denyCommands?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GatewayUnhandledRejectionsMode = "exit" | "warn";
|
||||||
|
|
||||||
export type GatewayConfig = {
|
export type GatewayConfig = {
|
||||||
/** Single multiplexed port for Gateway WS + HTTP (default: 18789). */
|
/** Single multiplexed port for Gateway WS + HTTP (default: 18789). */
|
||||||
port?: number;
|
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.
|
* Explicit gateway mode. When set to "remote", local gateway start is disabled.
|
||||||
* When set to "local", the CLI may start the gateway locally.
|
* When set to "local", the CLI may start the gateway locally.
|
||||||
|
|||||||
@ -304,6 +304,7 @@ export const MoltbotSchema = z
|
|||||||
.object({
|
.object({
|
||||||
port: z.number().int().positive().optional(),
|
port: z.number().int().positive().optional(),
|
||||||
mode: z.union([z.literal("local"), z.literal("remote")]).optional(),
|
mode: z.union([z.literal("local"), z.literal("remote")]).optional(),
|
||||||
|
unhandledRejections: z.union([z.literal("exit"), z.literal("warn")]).optional(),
|
||||||
bind: z
|
bind: z
|
||||||
.union([
|
.union([
|
||||||
z.literal("auto"),
|
z.literal("auto"),
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import {
|
|||||||
import { assertSupportedRuntime } from "./infra/runtime-guard.js";
|
import { assertSupportedRuntime } from "./infra/runtime-guard.js";
|
||||||
import { formatUncaughtError } from "./infra/errors.js";
|
import { formatUncaughtError } from "./infra/errors.js";
|
||||||
import { installUnhandledRejectionHandler } from "./infra/unhandled-rejections.js";
|
import { installUnhandledRejectionHandler } from "./infra/unhandled-rejections.js";
|
||||||
|
import { loadConfig } from "./config/config.js";
|
||||||
import { enableConsoleCapture } from "./logging.js";
|
import { enableConsoleCapture } from "./logging.js";
|
||||||
import { runCommandWithTimeout, runExec } from "./process/exec.js";
|
import { runCommandWithTimeout, runExec } from "./process/exec.js";
|
||||||
import { assertWebChannel, normalizeE164, toWhatsappJid } from "./utils.js";
|
import { assertWebChannel, normalizeE164, toWhatsappJid } from "./utils.js";
|
||||||
@ -79,8 +80,10 @@ const isMain = isMainModule({
|
|||||||
|
|
||||||
if (isMain) {
|
if (isMain) {
|
||||||
// Global error handlers to prevent silent crashes from unhandled rejections/exceptions.
|
// Global error handlers to prevent silent crashes from unhandled rejections/exceptions.
|
||||||
// These log the error and exit gracefully instead of crashing without trace.
|
// Default behavior is "exit" (preserves historical behavior). Users can opt into
|
||||||
installUnhandledRejectionHandler();
|
// "warn" via config: gateway.unhandledRejections.
|
||||||
|
const cfg = loadConfig();
|
||||||
|
installUnhandledRejectionHandler({ mode: cfg.gateway?.unhandledRejections ?? "exit" });
|
||||||
|
|
||||||
process.on("uncaughtException", (error) => {
|
process.on("uncaughtException", (error) => {
|
||||||
console.error("[moltbot] Uncaught exception:", formatUncaughtError(error));
|
console.error("[moltbot] Uncaught exception:", formatUncaughtError(error));
|
||||||
|
|||||||
@ -123,7 +123,11 @@ export function isUnhandledRejectionHandled(reason: unknown): boolean {
|
|||||||
return false;
|
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) => {
|
process.on("unhandledRejection", (reason, _promise) => {
|
||||||
if (isUnhandledRejectionHandled(reason)) return;
|
if (isUnhandledRejectionHandled(reason)) return;
|
||||||
|
|
||||||
@ -155,6 +159,7 @@ export function installUnhandledRejectionHandler(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.error("[moltbot] Unhandled promise rejection:", formatUncaughtError(reason));
|
console.error("[moltbot] Unhandled promise rejection:", formatUncaughtError(reason));
|
||||||
|
if (mode === "warn") return;
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user