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:
parent
eb50314d7d
commit
558b64f5fa
@ -322,7 +322,7 @@ export const registerTelegramNativeCommands = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (allCommands.length > 0) {
|
if (allCommands.length > 0) {
|
||||||
void withTelegramApiErrorLogging({
|
withTelegramApiErrorLogging({
|
||||||
operation: "setMyCommands",
|
operation: "setMyCommands",
|
||||||
runtime,
|
runtime,
|
||||||
fn: () => bot.api.setMyCommands(allCommands),
|
fn: () => bot.api.setMyCommands(allCommands),
|
||||||
@ -576,7 +576,7 @@ export const registerTelegramNativeCommands = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (nativeDisabledExplicit) {
|
} else if (nativeDisabledExplicit) {
|
||||||
void withTelegramApiErrorLogging({
|
withTelegramApiErrorLogging({
|
||||||
operation: "setMyCommands",
|
operation: "setMyCommands",
|
||||||
runtime,
|
runtime,
|
||||||
fn: () => bot.api.setMyCommands([]),
|
fn: () => bot.api.setMyCommands([]),
|
||||||
|
|||||||
@ -74,6 +74,23 @@ const isGetUpdatesConflict = (err: unknown) => {
|
|||||||
return haystack.includes("getupdates");
|
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 = {}) {
|
export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
|
||||||
const cfg = opts.config ?? loadConfig();
|
const cfg = opts.config ?? loadConfig();
|
||||||
const account = resolveTelegramAccount({
|
const account = resolveTelegramAccount({
|
||||||
@ -158,7 +175,8 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
|
|||||||
}
|
}
|
||||||
const isConflict = isGetUpdatesConflict(err);
|
const isConflict = isGetUpdatesConflict(err);
|
||||||
const isRecoverable = isRecoverableTelegramNetworkError(err, { context: "polling" });
|
const isRecoverable = isRecoverableTelegramNetworkError(err, { context: "polling" });
|
||||||
if (!isConflict && !isRecoverable) {
|
const isNetworkError = isNetworkRelatedError(err);
|
||||||
|
if (!isConflict && !isRecoverable && !isNetworkError) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
restartAttempts += 1;
|
restartAttempts += 1;
|
||||||
|
|||||||
@ -8,6 +8,13 @@ describe("isRecoverableTelegramNetworkError", () => {
|
|||||||
expect(isRecoverableTelegramNetworkError(err)).toBe(true);
|
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", () => {
|
it("detects AbortError names", () => {
|
||||||
const err = Object.assign(new Error("The operation was aborted"), { name: "AbortError" });
|
const err = Object.assign(new Error("The operation was aborted"), { name: "AbortError" });
|
||||||
expect(isRecoverableTelegramNetworkError(err)).toBe(true);
|
expect(isRecoverableTelegramNetworkError(err)).toBe(true);
|
||||||
@ -19,6 +26,11 @@ describe("isRecoverableTelegramNetworkError", () => {
|
|||||||
expect(isRecoverableTelegramNetworkError(err)).toBe(true);
|
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", () => {
|
it("skips message matches for send context", () => {
|
||||||
const err = new TypeError("fetch failed");
|
const err = new TypeError("fetch failed");
|
||||||
expect(isRecoverableTelegramNetworkError(err, { context: "send" })).toBe(false);
|
expect(isRecoverableTelegramNetworkError(err, { context: "send" })).toBe(false);
|
||||||
|
|||||||
@ -15,6 +15,8 @@ const RECOVERABLE_ERROR_CODES = new Set([
|
|||||||
"UND_ERR_BODY_TIMEOUT",
|
"UND_ERR_BODY_TIMEOUT",
|
||||||
"UND_ERR_SOCKET",
|
"UND_ERR_SOCKET",
|
||||||
"UND_ERR_ABORTED",
|
"UND_ERR_ABORTED",
|
||||||
|
"ECONNABORTED",
|
||||||
|
"ERR_NETWORK",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const RECOVERABLE_ERROR_NAMES = new Set([
|
const RECOVERABLE_ERROR_NAMES = new Set([
|
||||||
@ -27,6 +29,8 @@ const RECOVERABLE_ERROR_NAMES = new Set([
|
|||||||
|
|
||||||
const RECOVERABLE_MESSAGE_SNIPPETS = [
|
const RECOVERABLE_MESSAGE_SNIPPETS = [
|
||||||
"fetch failed",
|
"fetch failed",
|
||||||
|
"typeerror: fetch failed",
|
||||||
|
"undici",
|
||||||
"network error",
|
"network error",
|
||||||
"network request",
|
"network request",
|
||||||
"client network socket disconnected",
|
"client network socket disconnected",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user