- 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.
74 lines
2.3 KiB
TypeScript
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);
|
|
});
|
|
}
|