fix(telegram): retain socket failure context (#97130)

Co-authored-by: zhang-guiping <zhang.guiping@xydigit.com>
This commit is contained in:
Peter Steinberger
2026-06-27 02:11:36 +01:00
committed by GitHub
parent 1bccd29304
commit 072d3ed7b5
4 changed files with 36 additions and 8 deletions
+26 -1
View File
@@ -957,7 +957,7 @@ describe("resolveTelegramFetch", () => {
expect(eighthDispatcher).toBe(firstDispatcher);
expect(ninthDispatcher).toBe(firstDispatcher);
expectPinnedFallbackIpDispatcher(3);
expectLoggerMessageContaining(loggerWarn, "fetch fallback: DNS-resolved IP unreachable");
expectLoggerMessageContaining(loggerWarn, "fetch fallback: primary connection path failed");
expectLoggerMessageContaining(
loggerDebug,
"fetch fallback: recovered from attempt 2 to attempt 0",
@@ -1193,6 +1193,31 @@ describe("resolveTelegramFetch", () => {
expect(undiciFetch).toHaveBeenCalledTimes(1);
});
it("does not automatically retry structured EADDRNOTAVAIL fetch failures", async () => {
const fetchError = buildFetchFallbackError("EADDRNOTAVAIL");
undiciFetch.mockRejectedValue(fetchError);
const resolved = resolveTelegramFetchOrThrow(undefined, STICKY_IPV4_FALLBACK_NETWORK);
await expect(resolved("https://api.telegram.org/botx/sendMessage")).rejects.toThrow(
"fetch failed",
);
expect(undiciFetch).toHaveBeenCalledTimes(1);
});
it("preserves EADDRNOTAVAIL in forced fallback diagnostics", () => {
const transport = resolveTelegramTransport(undefined, STICKY_IPV4_FALLBACK_NETWORK);
const fetchError = buildFetchFallbackError("EADDRNOTAVAIL");
expect(transport.forceFallback?.("probe timeout/network error", fetchError)).toBe(true);
expect(transport.forceFallback?.("probe timeout/network error", fetchError)).toBe(true);
expectLoggerMessageContaining(loggerWarn, "primary connection path failed");
expectLoggerMessageContaining(loggerWarn, "codes=EADDRNOTAVAIL");
expectNoLoggerMessageContaining(loggerWarn, "DNS-resolved IP unreachable");
});
it("retries sticky fallback when the local network is down during connect", async () => {
undiciFetch
.mockRejectedValueOnce(buildFetchFallbackError("ENETDOWN"))
+7 -5
View File
@@ -488,9 +488,10 @@ export type TelegramTransport = {
dispatcherAttempts?: TelegramDispatcherAttempt[];
/**
* Promote this transport to its next fallback dispatcher before the next
* request. Returns false when no fallback path exists.
* request. The original error, when available, is retained in diagnostics.
* Returns false when no fallback path exists.
*/
forceFallback?: (reason: string) => boolean;
forceFallback?: (reason: string, err?: unknown) => boolean;
/**
* Release all dispatchers owned by this transport and the TCP sockets they
* hold. Safe to call multiple times; subsequent calls resolve immediately.
@@ -563,7 +564,8 @@ function createTelegramTransportAttempts(params: {
},
exportAttempt: { dispatcherPolicy: fallbackIpPolicy },
logLevel: "warn",
logMessage: "fetch fallback: DNS-resolved IP unreachable; trying alternative Telegram API IP",
logMessage:
"fetch fallback: primary connection path failed; trying alternative Telegram API IP",
});
return attempts;
@@ -864,8 +866,8 @@ export function resolveTelegramTransport(
fetch: resolvedFetch,
sourceFetch,
dispatcherAttempts: transportAttempts.map((attempt) => attempt.exportAttempt),
forceFallback: (reason: string) =>
promoteStickyAttempt(stickyAttemptIndex + 1, new Error("forced fallback"), reason),
forceFallback: (reason: string, err?: unknown) =>
promoteStickyAttempt(stickyAttemptIndex + 1, err ?? new Error("forced fallback"), reason),
close,
};
}
+1 -1
View File
@@ -362,7 +362,7 @@ describe("probeTelegram retry logic", () => {
const result = await probePromise;
expect(result.ok).toBe(true);
expect(localForceFallback).toHaveBeenCalledWith("probe timeout/network error");
expect(localForceFallback).toHaveBeenCalledWith("probe timeout/network error", timeoutError);
expect(fetchMock).toHaveBeenCalledTimes(3); // 1 failed + 1 getMe success + 1 webhook
} finally {
vi.useRealTimers();
+2 -1
View File
@@ -162,7 +162,8 @@ export async function probeTelegram(
// On timeout or network error, promote the transport to its IPv4
// fallback dispatcher so the next retry (and all future probes
// sharing this cached transport) skip the stalled IPv6 path.
transport.forceFallback?.("probe timeout/network error");
// Keep the original socket code in transport fallback diagnostics.
transport.forceFallback?.("probe timeout/network error", err);
if (i < 2) {
const remainingAfterAttemptMs = resolveRemainingBudgetMs();
if (remainingAfterAttemptMs <= 0) {