fix(telegram): use retry logic for sticker getFile calls (#32349)

The sticker code path called ctx.getFile() directly without retry,
unlike the non-sticker media path which uses resolveTelegramFileWithRetry
(3 attempts with jitter). This made sticker downloads vulnerable to
transient Telegram API failures, particularly in group topics where
file availability can be delayed.

Refs #32326

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
scoootscooob
2026-03-02 17:00:31 -08:00
committed by GitHub
parent 503d395780
commit de09ca149f
2 changed files with 53 additions and 3 deletions

View File

@@ -31,7 +31,7 @@ const MAX_MEDIA_BYTES = 10_000_000;
const BOT_TOKEN = "tok123";
function makeCtx(
mediaField: "voice" | "audio" | "photo" | "video" | "document" | "animation",
mediaField: "voice" | "audio" | "photo" | "video" | "document" | "animation" | "sticker",
getFile: TelegramContext["getFile"],
opts?: { file_name?: string },
): TelegramContext {
@@ -79,6 +79,17 @@ function makeCtx(
...(opts?.file_name && { file_name: opts.file_name }),
};
}
if (mediaField === "sticker") {
msg.sticker = {
file_id: "stk1",
file_unique_id: "ustk1",
type: "regular",
width: 512,
height: 512,
is_animated: false,
is_video: false,
};
}
return {
message: msg as unknown as Message,
me: {
@@ -243,6 +254,45 @@ describe("resolveMedia getFile retry", () => {
// Should retry transient errors.
expect(result).not.toBeNull();
});
it("retries getFile for stickers on transient failure", async () => {
const getFile = vi
.fn()
.mockRejectedValueOnce(new Error("Network request for 'getFile' failed!"))
.mockResolvedValueOnce({ file_path: "stickers/file_0.webp" });
fetchRemoteMedia.mockResolvedValueOnce({
buffer: Buffer.from("sticker-data"),
contentType: "image/webp",
fileName: "file_0.webp",
});
saveMediaBuffer.mockResolvedValueOnce({
path: "/tmp/file_0.webp",
contentType: "image/webp",
});
const ctx = makeCtx("sticker", getFile);
const promise = resolveMedia(ctx, MAX_MEDIA_BYTES, BOT_TOKEN);
await flushRetryTimers();
const result = await promise;
expect(getFile).toHaveBeenCalledTimes(2);
expect(result).toEqual(
expect.objectContaining({ path: "/tmp/file_0.webp", placeholder: "<media:sticker>" }),
);
});
it("returns null for sticker when getFile exhausts retries", async () => {
const getFile = vi.fn().mockRejectedValue(new Error("Network request for 'getFile' failed!"));
const ctx = makeCtx("sticker", getFile);
const promise = resolveMedia(ctx, MAX_MEDIA_BYTES, BOT_TOKEN);
await flushRetryTimers();
const result = await promise;
expect(getFile).toHaveBeenCalledTimes(3);
expect(result).toBeNull();
});
});
describe("resolveMedia original filename preservation", () => {

View File

@@ -156,8 +156,8 @@ async function resolveStickerMedia(params: {
}
try {
const file = await ctx.getFile();
if (!file.file_path) {
const file = await resolveTelegramFileWithRetry(ctx);
if (!file?.file_path) {
logVerbose("telegram: getFile returned no file_path for sticker");
return null;
}