Files
openclaw/src/telegram/network-errors.ts
2026-03-02 19:57:33 +00:00

118 lines
3.0 KiB
TypeScript

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<unknown> = [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;
}