grammY getUpdates returns "Request to getUpdates timed out after 500 seconds"
but RECOVERABLE_MESSAGE_SNIPPETS only had "timeout". Since
"timed out".includes("timeout") === false, the error was not classified as
recoverable, causing the polling loop to exit permanently.
Add "timed out" to RECOVERABLE_MESSAGE_SNIPPETS so the polling loop retries
instead of dying silently.
Fixes #7239
Fixes #7255
87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { isRecoverableTelegramNetworkError } from "./network-errors.js";
|
|
|
|
describe("isRecoverableTelegramNetworkError", () => {
|
|
it("detects recoverable error codes", () => {
|
|
const err = Object.assign(new Error("timeout"), { code: "ETIMEDOUT" });
|
|
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);
|
|
});
|
|
|
|
it("detects nested causes", () => {
|
|
const cause = Object.assign(new Error("socket hang up"), { code: "ECONNRESET" });
|
|
const err = Object.assign(new TypeError("fetch failed"), { cause });
|
|
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);
|
|
expect(isRecoverableTelegramNetworkError(err, { context: "polling" })).toBe(true);
|
|
});
|
|
|
|
it("returns false for unrelated errors", () => {
|
|
expect(isRecoverableTelegramNetworkError(new Error("invalid token"))).toBe(false);
|
|
});
|
|
|
|
it("detects grammY 'timed out' long-poll errors (#7239)", () => {
|
|
const err = new Error("Request to 'getUpdates' timed out after 500 seconds");
|
|
expect(isRecoverableTelegramNetworkError(err)).toBe(true);
|
|
});
|
|
|
|
// Grammy HttpError tests (issue #3815)
|
|
// Grammy wraps fetch errors in .error property, not .cause
|
|
describe("Grammy HttpError", () => {
|
|
class MockHttpError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public readonly error: unknown,
|
|
) {
|
|
super(message);
|
|
this.name = "HttpError";
|
|
}
|
|
}
|
|
|
|
it("detects network error wrapped in HttpError", () => {
|
|
const fetchError = new TypeError("fetch failed");
|
|
const httpError = new MockHttpError(
|
|
"Network request for 'setMyCommands' failed!",
|
|
fetchError,
|
|
);
|
|
|
|
expect(isRecoverableTelegramNetworkError(httpError)).toBe(true);
|
|
});
|
|
|
|
it("detects network error with cause wrapped in HttpError", () => {
|
|
const cause = Object.assign(new Error("socket hang up"), { code: "ECONNRESET" });
|
|
const fetchError = Object.assign(new TypeError("fetch failed"), { cause });
|
|
const httpError = new MockHttpError("Network request for 'getUpdates' failed!", fetchError);
|
|
|
|
expect(isRecoverableTelegramNetworkError(httpError)).toBe(true);
|
|
});
|
|
|
|
it("returns false for non-network errors wrapped in HttpError", () => {
|
|
const authError = new Error("Unauthorized: bot token is invalid");
|
|
const httpError = new MockHttpError("Bad Request: invalid token", authError);
|
|
|
|
expect(isRecoverableTelegramNetworkError(httpError)).toBe(false);
|
|
});
|
|
});
|
|
});
|