Files
clawdbot/src/infra/unhandled-rejections.ts
techboss e43f4c0628 fix(telegram): handle network errors gracefully
- Add bot.catch() to prevent unhandled rejections from middleware
- Add isRecoverableNetworkError() to retry on transient failures
- Add maxRetryTime and exponential backoff to grammY runner
- Global unhandled rejection handler now logs recoverable errors
  instead of crashing (fetch failures, timeouts, connection resets)

Fixes crash loop when Telegram API is temporarily unreachable.
2026-01-26 19:36:43 -05:00

74 lines
2.3 KiB
TypeScript

import process from "node:process";
import { formatUncaughtError } from "./errors.js";
type UnhandledRejectionHandler = (reason: unknown) => boolean;
const handlers = new Set<UnhandledRejectionHandler>();
export function registerUnhandledRejectionHandler(handler: UnhandledRejectionHandler): () => void {
handlers.add(handler);
return () => {
handlers.delete(handler);
};
}
/**
* Check if an error is a recoverable/transient error that shouldn't crash the process.
* These include network errors and abort signals during shutdown.
*/
function isRecoverableError(reason: unknown): boolean {
if (!reason) return false;
// Check error name for AbortError
if (reason instanceof Error && reason.name === "AbortError") {
return true;
}
const message = reason instanceof Error ? reason.message : String(reason);
const lowerMessage = message.toLowerCase();
return (
lowerMessage.includes("fetch failed") ||
lowerMessage.includes("network request") ||
lowerMessage.includes("econnrefused") ||
lowerMessage.includes("econnreset") ||
lowerMessage.includes("etimedout") ||
lowerMessage.includes("socket hang up") ||
lowerMessage.includes("enotfound") ||
lowerMessage.includes("network error") ||
lowerMessage.includes("getaddrinfo") ||
lowerMessage.includes("client network socket disconnected") ||
lowerMessage.includes("this operation was aborted") ||
lowerMessage.includes("aborted")
);
}
export function isUnhandledRejectionHandled(reason: unknown): boolean {
for (const handler of handlers) {
try {
if (handler(reason)) return true;
} catch (err) {
console.error(
"[clawdbot] Unhandled rejection handler failed:",
err instanceof Error ? (err.stack ?? err.message) : err,
);
}
}
return false;
}
export function installUnhandledRejectionHandler(): void {
process.on("unhandledRejection", (reason, _promise) => {
if (isUnhandledRejectionHandled(reason)) return;
// Don't crash on recoverable/transient errors - log them and continue
if (isRecoverableError(reason)) {
console.error("[clawdbot] Recoverable error (not crashing):", formatUncaughtError(reason));
return;
}
console.error("[clawdbot] Unhandled promise rejection:", formatUncaughtError(reason));
process.exit(1);
});
}