mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(telegram): treat ENETDOWN as transient network failure (#86762)
This commit is contained in:
@@ -1101,6 +1101,23 @@ describe("resolveTelegramFetch", () => {
|
||||
expect(undiciFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("retries sticky fallback when the local network is down during connect", async () => {
|
||||
undiciFetch
|
||||
.mockRejectedValueOnce(buildFetchFallbackError("ENETDOWN"))
|
||||
.mockResolvedValueOnce({ ok: true } as Response);
|
||||
|
||||
const resolved = resolveTelegramFetchOrThrow(undefined, {
|
||||
network: {
|
||||
autoSelectFamily: true,
|
||||
},
|
||||
});
|
||||
|
||||
await resolved("https://api.telegram.org/botx/getUpdates");
|
||||
|
||||
expect(undiciFetch).toHaveBeenCalledTimes(2);
|
||||
expect(getDispatcherFromUndiciCall(1)).not.toBe(getDispatcherFromUndiciCall(2));
|
||||
});
|
||||
|
||||
it("keeps per-resolver transport policy isolated across multiple accounts", async () => {
|
||||
undiciFetch.mockResolvedValue({ ok: true } as Response);
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ type LookupFunction = (
|
||||
|
||||
const FALLBACK_RETRY_ERROR_CODES = [
|
||||
"ETIMEDOUT",
|
||||
"ENETDOWN",
|
||||
"ENETUNREACH",
|
||||
"EHOSTUNREACH",
|
||||
"UND_ERR_CONNECT_TIMEOUT",
|
||||
|
||||
@@ -83,6 +83,7 @@ describe("isRecoverableTelegramNetworkError", () => {
|
||||
|
||||
it.each([
|
||||
["ETIMEDOUT", "timeout"],
|
||||
["ENETDOWN", "network down"],
|
||||
["ECONNABORTED", "aborted"],
|
||||
["ERR_NETWORK", "network"],
|
||||
])("detects recoverable error code %s", (code, message) => {
|
||||
@@ -218,6 +219,7 @@ describe("isSafeToRetrySendError", () => {
|
||||
["ECONNREFUSED", "connect ECONNREFUSED", true],
|
||||
["ENOTFOUND", "getaddrinfo ENOTFOUND", true],
|
||||
["EAI_AGAIN", "getaddrinfo EAI_AGAIN", true],
|
||||
["ENETDOWN", "connect ENETDOWN", true],
|
||||
["ENETUNREACH", "connect ENETUNREACH", true],
|
||||
["EHOSTUNREACH", "connect EHOSTUNREACH", true],
|
||||
["ECONNRESET", "read ECONNRESET", false],
|
||||
|
||||
@@ -12,6 +12,7 @@ const RECOVERABLE_ERROR_CODES = new Set([
|
||||
"ECONNRESET",
|
||||
"ECONNREFUSED",
|
||||
"EPIPE",
|
||||
"ENETDOWN",
|
||||
"ETIMEDOUT",
|
||||
"ESOCKETTIMEDOUT",
|
||||
"ENETUNREACH",
|
||||
@@ -41,6 +42,7 @@ const PRE_CONNECT_ERROR_CODES = new Set([
|
||||
"ECONNREFUSED", // Server actively refused the connection (never reached Telegram)
|
||||
"ENOTFOUND", // DNS resolution failed (never sent)
|
||||
"EAI_AGAIN", // Transient DNS failure (never sent)
|
||||
"ENETDOWN", // Local network interface is down before connect completes (never sent)
|
||||
"ENETUNREACH", // No route to host (never sent)
|
||||
"EHOSTUNREACH", // Host unreachable (never sent)
|
||||
]);
|
||||
|
||||
@@ -16,6 +16,7 @@ describe("resolveCronExecutionRetryHint", () => {
|
||||
it("treats common network error codes as network when retryOn only includes network", () => {
|
||||
for (const code of [
|
||||
"EAI_AGAIN",
|
||||
"ENETDOWN",
|
||||
"EHOSTUNREACH",
|
||||
"EHOSTDOWN",
|
||||
"ENETRESET",
|
||||
|
||||
@@ -11,7 +11,7 @@ const TRANSIENT_PATTERNS: Record<CronRetryOn, RegExp> = {
|
||||
overloaded:
|
||||
/\b529\b|\boverloaded(?:_error)?\b|high demand|temporar(?:ily|y) overloaded|capacity exceeded/i,
|
||||
network:
|
||||
/(network|fetch failed|socket|econnreset|econnrefused|eai_again|ehostunreach|ehostdown|enetreset|enetunreach|epipe)/i,
|
||||
/(network|fetch failed|socket|econnreset|econnrefused|eai_again|enetdown|ehostunreach|ehostdown|enetreset|enetunreach|epipe)/i,
|
||||
timeout: /(timeout|etimedout)/i,
|
||||
server_error: /\b5\d{2}\b/,
|
||||
};
|
||||
|
||||
@@ -150,6 +150,9 @@ describe("installUnhandledRejectionHandler - fatal detection", () => {
|
||||
cause: { code: "UND_ERR_CONNECT_TIMEOUT", syscall: "connect" },
|
||||
}),
|
||||
Object.assign(new Error("DNS resolve failed"), { code: "UND_ERR_DNS_RESOLVE_FAILED" }),
|
||||
Object.assign(new Error("connect ENETDOWN 149.154.167.220:443"), {
|
||||
code: "ENETDOWN",
|
||||
}),
|
||||
Object.assign(new Error("Connection reset"), { code: "ECONNRESET" }),
|
||||
Object.assign(new Error("Timeout"), { code: "ETIMEDOUT" }),
|
||||
Object.assign(
|
||||
|
||||
@@ -60,6 +60,7 @@ describe("isTransientNetworkError", () => {
|
||||
"ESOCKETTIMEDOUT",
|
||||
"ECONNABORTED",
|
||||
"EPIPE",
|
||||
"ENETDOWN",
|
||||
"EHOSTUNREACH",
|
||||
"ENETUNREACH",
|
||||
"EADDRNOTAVAIL",
|
||||
@@ -375,6 +376,12 @@ describe("isTransientUnhandledRejectionError", () => {
|
||||
const epipe = Object.assign(new Error("write EPIPE"), { code: "EPIPE" });
|
||||
const sqlite = Object.assign(new Error("database is locked"), { code: "SQLITE_BUSY" });
|
||||
const network = Object.assign(new Error("connection reset"), { code: "ECONNRESET" });
|
||||
const networkDown = Object.assign(new Error("connect ENETDOWN"), {
|
||||
code: "ENETDOWN",
|
||||
});
|
||||
const rawNetworkDown = new Error(
|
||||
"connect ENETDOWN 149.154.167.220:443 - Local (10.0.10.40:50017)",
|
||||
);
|
||||
const hostUnreachable = Object.assign(new Error("connect EHOSTUNREACH"), {
|
||||
code: "EHOSTUNREACH",
|
||||
});
|
||||
@@ -398,6 +405,8 @@ describe("isTransientUnhandledRejectionError", () => {
|
||||
expect(isBenignUncaughtExceptionError(epipe)).toBe(true);
|
||||
expect(isBenignUncaughtExceptionError(sqlite)).toBe(false);
|
||||
expect(isBenignUncaughtExceptionError(network)).toBe(false);
|
||||
expect(isBenignUncaughtExceptionError(networkDown)).toBe(true);
|
||||
expect(isBenignUncaughtExceptionError(rawNetworkDown)).toBe(true);
|
||||
expect(isBenignUncaughtExceptionError(hostUnreachable)).toBe(true);
|
||||
expect(isBenignUncaughtExceptionError(rawHostUnreachable)).toBe(true);
|
||||
expect(isBenignUncaughtExceptionError(addressUnavailable)).toBe(true);
|
||||
|
||||
@@ -57,6 +57,7 @@ const TRANSIENT_NETWORK_CODES = new Set([
|
||||
"ESOCKETTIMEDOUT",
|
||||
"ECONNABORTED",
|
||||
"EPIPE",
|
||||
"ENETDOWN",
|
||||
"EHOSTUNREACH",
|
||||
"ENETUNREACH",
|
||||
"EADDRNOTAVAIL",
|
||||
@@ -93,6 +94,7 @@ const TRANSIENT_SQLITE_ERRCODES = new Set([5, 6, 10, 14]);
|
||||
const BENIGN_UNCAUGHT_EXCEPTION_CODES = new Set(["EPIPE", "EIO"]);
|
||||
const BENIGN_UNCAUGHT_EXCEPTION_NETWORK_CODES = new Set([
|
||||
"ECONNREFUSED",
|
||||
"ENETDOWN",
|
||||
"EHOSTUNREACH",
|
||||
"ENETUNREACH",
|
||||
"EADDRNOTAVAIL",
|
||||
@@ -106,9 +108,9 @@ const BENIGN_UNCAUGHT_EXCEPTION_NETWORK_CODES = new Set([
|
||||
]);
|
||||
|
||||
const TRANSIENT_NETWORK_MESSAGE_CODE_RE =
|
||||
/\b(ECONNRESET|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ESOCKETTIMEDOUT|ECONNABORTED|EPIPE|EHOSTUNREACH|ENETUNREACH|EADDRNOTAVAIL|EAI_AGAIN|EPROTO|UND_ERR_CONNECT_TIMEOUT|UND_ERR_DNS_RESOLVE_FAILED|UND_ERR_CONNECT|UND_ERR_SOCKET|UND_ERR_HEADERS_TIMEOUT|UND_ERR_BODY_TIMEOUT|ERR_HTTP2_INVALID_SESSION)\b/i;
|
||||
/\b(ECONNRESET|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ESOCKETTIMEDOUT|ECONNABORTED|EPIPE|ENETDOWN|EHOSTUNREACH|ENETUNREACH|EADDRNOTAVAIL|EAI_AGAIN|EPROTO|UND_ERR_CONNECT_TIMEOUT|UND_ERR_DNS_RESOLVE_FAILED|UND_ERR_CONNECT|UND_ERR_SOCKET|UND_ERR_HEADERS_TIMEOUT|UND_ERR_BODY_TIMEOUT|ERR_HTTP2_INVALID_SESSION)\b/i;
|
||||
const BENIGN_UNCAUGHT_EXCEPTION_NETWORK_MESSAGE_CODE_RE =
|
||||
/\b(ECONNREFUSED|EHOSTUNREACH|ENETUNREACH|EADDRNOTAVAIL|EAI_AGAIN|ENOTFOUND|ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT|UND_ERR_DNS_RESOLVE_FAILED|UND_ERR_CONNECT|ERR_HTTP2_INVALID_SESSION)\b/i;
|
||||
/\b(ECONNREFUSED|ENETDOWN|EHOSTUNREACH|ENETUNREACH|EADDRNOTAVAIL|EAI_AGAIN|ENOTFOUND|ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT|UND_ERR_DNS_RESOLVE_FAILED|UND_ERR_CONNECT|ERR_HTTP2_INVALID_SESSION)\b/i;
|
||||
|
||||
const TRANSIENT_SQLITE_MESSAGE_CODE_RE =
|
||||
/\b(SQLITE_BUSY|SQLITE_CANTOPEN|SQLITE_IOERR|SQLITE_LOCKED)\b/i;
|
||||
|
||||
Reference in New Issue
Block a user