fix: handle Telegram network errors gracefully to prevent gateway crashes

- Expand recoverable error codes (ECONNABORTED, ERR_NETWORK)
- Add message patterns for 'typeerror: fetch failed' and 'undici' errors
- Add isNetworkRelatedError() helper for broad network failure detection
- Retry on all network-related errors instead of crashing gateway
- Remove unnecessary 'void' from fire-and-forget patterns
- Add tests for new error patterns

Fixes #3005
This commit is contained in:
ryan
2026-01-28 11:33:51 +13:00
committed by Shadow
parent eb50314d7d
commit 558b64f5fa
4 changed files with 37 additions and 3 deletions

View File

@@ -322,7 +322,7 @@ export const registerTelegramNativeCommands = ({
];
if (allCommands.length > 0) {
void withTelegramApiErrorLogging({
withTelegramApiErrorLogging({
operation: "setMyCommands",
runtime,
fn: () => bot.api.setMyCommands(allCommands),
@@ -576,7 +576,7 @@ export const registerTelegramNativeCommands = ({
}
}
} else if (nativeDisabledExplicit) {
void withTelegramApiErrorLogging({
withTelegramApiErrorLogging({
operation: "setMyCommands",
runtime,
fn: () => bot.api.setMyCommands([]),

View File

@@ -74,6 +74,23 @@ const isGetUpdatesConflict = (err: unknown) => {
return haystack.includes("getupdates");
};
const NETWORK_ERROR_SNIPPETS = [
"fetch failed",
"network",
"timeout",
"socket",
"econnreset",
"econnrefused",
"undici",
];
const isNetworkRelatedError = (err: unknown) => {
if (!err) return false;
const message = formatErrorMessage(err).toLowerCase();
if (!message) return false;
return NETWORK_ERROR_SNIPPETS.some((snippet) => message.includes(snippet));
};
export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
const cfg = opts.config ?? loadConfig();
const account = resolveTelegramAccount({
@@ -158,7 +175,8 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
}
const isConflict = isGetUpdatesConflict(err);
const isRecoverable = isRecoverableTelegramNetworkError(err, { context: "polling" });
if (!isConflict && !isRecoverable) {
const isNetworkError = isNetworkRelatedError(err);
if (!isConflict && !isRecoverable && !isNetworkError) {
throw err;
}
restartAttempts += 1;

View File

@@ -8,6 +8,13 @@ describe("isRecoverableTelegramNetworkError", () => {
expect(isRecoverableTelegramNetworkError(err)).toBe(true);
});
it("detects additional recoverable error codes", () => {
const aborted = Object.assign(new Error("aborted"), { code: "ECONNABORTED" });
const network = Object.assign(new Error("network"), { code: "ERR_NETWORK" });
expect(isRecoverableTelegramNetworkError(aborted)).toBe(true);
expect(isRecoverableTelegramNetworkError(network)).toBe(true);
});
it("detects AbortError names", () => {
const err = Object.assign(new Error("The operation was aborted"), { name: "AbortError" });
expect(isRecoverableTelegramNetworkError(err)).toBe(true);
@@ -19,6 +26,11 @@ describe("isRecoverableTelegramNetworkError", () => {
expect(isRecoverableTelegramNetworkError(err)).toBe(true);
});
it("detects expanded message patterns", () => {
expect(isRecoverableTelegramNetworkError(new Error("TypeError: fetch failed"))).toBe(true);
expect(isRecoverableTelegramNetworkError(new Error("Undici: socket failure"))).toBe(true);
});
it("skips message matches for send context", () => {
const err = new TypeError("fetch failed");
expect(isRecoverableTelegramNetworkError(err, { context: "send" })).toBe(false);

View File

@@ -15,6 +15,8 @@ const RECOVERABLE_ERROR_CODES = new Set([
"UND_ERR_BODY_TIMEOUT",
"UND_ERR_SOCKET",
"UND_ERR_ABORTED",
"ECONNABORTED",
"ERR_NETWORK",
]);
const RECOVERABLE_ERROR_NAMES = new Set([
@@ -27,6 +29,8 @@ const RECOVERABLE_ERROR_NAMES = new Set([
const RECOVERABLE_MESSAGE_SNIPPETS = [
"fetch failed",
"typeerror: fetch failed",
"undici",
"network error",
"network request",
"client network socket disconnected",