fix(telegram): add UND_ERR_CONNECT_TIMEOUT to PRE_CONNECT_ERROR_CODES (#101258)

* fix(telegram): add UND_ERR_CONNECT_TIMEOUT to PRE_CONNECT_ERROR_CODES

UND_ERR_CONNECT_TIMEOUT occurs during TCP/TLS connect handshake,
before any HTTP request data is sent. Adding it to the pre-connect
set allows isSafeToRetrySendError to safely retry sendMessage when
undici's connect timeout fires — the message was never transmitted.

* test(telegram): cover connect-timeout retry funnels

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
lzw112
2026-07-07 12:33:02 +08:00
committed by GitHub
parent db42f05efe
commit da20b65231
4 changed files with 12 additions and 9 deletions
+6 -5
View File
@@ -218,9 +218,10 @@ function createHtmlParseError(operation = "sendMessage") {
);
}
function createWrappedPreConnectHttpError(operation = "sendMessage") {
const root = Object.assign(new Error("getaddrinfo ENOTFOUND api.telegram.org"), {
code: "ENOTFOUND",
function createWrappedConnectTimeoutHttpError(operation = "sendMessage") {
const root = Object.assign(new Error("Connect Timeout Error"), {
name: "ConnectTimeoutError",
code: "UND_ERR_CONNECT_TIMEOUT",
});
const fetchError = Object.assign(new TypeError("fetch failed"), { cause: root });
return Object.assign(new Error(`Network request for '${operation}' failed!`), {
@@ -1027,12 +1028,12 @@ describe("deliverReplies", () => {
expect(runtime.error).toHaveBeenCalledTimes(1);
});
it("retries final text sends for wrapped pre-connect grammY HttpError envelopes", async () => {
it("retries final text sends for wrapped Undici connect timeouts", async () => {
vi.useFakeTimers();
const runtime = createRuntime();
const sendMessage = vi
.fn()
.mockRejectedValueOnce(createWrappedPreConnectHttpError("sendMessage"))
.mockRejectedValueOnce(createWrappedConnectTimeoutHttpError("sendMessage"))
.mockResolvedValueOnce({
message_id: 12,
chat: { id: "123" },
@@ -239,7 +239,7 @@ describe("isSafeToRetrySendError", () => {
["ECONNRESET", "read ECONNRESET", false],
["ETIMEDOUT", "connect ETIMEDOUT", false],
["EPIPE", "write EPIPE", false],
["UND_ERR_CONNECT_TIMEOUT", "connect timeout", false],
["UND_ERR_CONNECT_TIMEOUT", "connect timeout", true],
])("returns %s => %s", (code, message, expected) => {
expect(isSafeToRetrySendError(errorWithCode(message, code))).toBe(expected);
});
@@ -47,6 +47,7 @@ const PRE_CONNECT_ERROR_CODES = new Set([
"ENETDOWN", // Local network interface is down before connect completes (never sent)
"ENETUNREACH", // No route to host (never sent)
"EHOSTUNREACH", // Host unreachable (never sent)
"UND_ERR_CONNECT_TIMEOUT", // TCP/TLS connect timeout (request never sent)
]);
const RECOVERABLE_ERROR_NAMES = new Set([
+4 -3
View File
@@ -2401,11 +2401,12 @@ describe("sendMessageTelegram", () => {
vi.useRealTimers();
});
it("retries wrapped pre-connect HttpError sends", async () => {
it("retries wrapped Undici connect timeout sends", async () => {
vi.useFakeTimers();
const chatId = "123";
const root = Object.assign(new Error("connect ECONNREFUSED api.telegram.org"), {
code: "ECONNREFUSED",
const root = Object.assign(new Error("Connect Timeout Error"), {
name: "ConnectTimeoutError",
code: "UND_ERR_CONNECT_TIMEOUT",
});
const fetchError = Object.assign(new TypeError("fetch failed"), { cause: root });
const err = Object.assign(new Error("Network request for 'sendMessage' failed!"), {