import { collectErrorGraphCandidates, extractErrorCode, formatErrorMessage, readErrorName, } from "../infra/errors.js"; const RECOVERABLE_ERROR_CODES = new Set([ "ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "ESOCKETTIMEDOUT", "ENETUNREACH", "EHOSTUNREACH", "ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "UND_ERR_SOCKET", "UND_ERR_ABORTED", "ECONNABORTED", "ERR_NETWORK", ]); const RECOVERABLE_ERROR_NAMES = new Set([ "AbortError", "TimeoutError", "ConnectTimeoutError", "HeadersTimeoutError", "BodyTimeoutError", ]); const ALWAYS_RECOVERABLE_MESSAGES = new Set(["fetch failed", "typeerror: fetch failed"]); const RECOVERABLE_MESSAGE_SNIPPETS = [ "undici", "network error", "network request", "client network socket disconnected", "socket hang up", "getaddrinfo", "timeout", // catch timeout messages not covered by error codes/names "timed out", // grammY getUpdates returns "timed out after X seconds" (not matched by "timeout") ]; function normalizeCode(code?: string): string { return code?.trim().toUpperCase() ?? ""; } function getErrorCode(err: unknown): string | undefined { const direct = extractErrorCode(err); if (direct) { return direct; } if (!err || typeof err !== "object") { return undefined; } const errno = (err as { errno?: unknown }).errno; if (typeof errno === "string") { return errno; } if (typeof errno === "number") { return String(errno); } return undefined; } export type TelegramNetworkErrorContext = "polling" | "send" | "webhook" | "unknown"; export function isRecoverableTelegramNetworkError( err: unknown, options: { context?: TelegramNetworkErrorContext; allowMessageMatch?: boolean } = {}, ): boolean { if (!err) { return false; } const allowMessageMatch = typeof options.allowMessageMatch === "boolean" ? options.allowMessageMatch : options.context !== "send"; for (const candidate of collectErrorGraphCandidates(err, (current) => { const nested: Array = [current.cause, current.reason]; if (Array.isArray(current.errors)) { nested.push(...current.errors); } // Grammy's HttpError wraps the underlying error in .error (not .cause). if (readErrorName(current) === "HttpError") { nested.push(current.error); } return nested; })) { const code = normalizeCode(getErrorCode(candidate)); if (code && RECOVERABLE_ERROR_CODES.has(code)) { return true; } const name = readErrorName(candidate); if (name && RECOVERABLE_ERROR_NAMES.has(name)) { return true; } const message = formatErrorMessage(candidate).trim().toLowerCase(); if (message && ALWAYS_RECOVERABLE_MESSAGES.has(message)) { return true; } if (allowMessageMatch && message) { if (RECOVERABLE_MESSAGE_SNIPPETS.some((snippet) => message.includes(snippet))) { return true; } } } return false; }