From de09ca149f9d0eac8bd35f4cae159ca582efd63f Mon Sep 17 00:00:00 2001 From: scoootscooob <167050519+scoootscooob@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:00:31 -0800 Subject: [PATCH] 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 --- .../bot/delivery.resolve-media-retry.test.ts | 52 ++++++++++++++++++- src/telegram/bot/delivery.resolve-media.ts | 4 +- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/telegram/bot/delivery.resolve-media-retry.test.ts b/src/telegram/bot/delivery.resolve-media-retry.test.ts index e265d265d70..ce8f50abbbe 100644 --- a/src/telegram/bot/delivery.resolve-media-retry.test.ts +++ b/src/telegram/bot/delivery.resolve-media-retry.test.ts @@ -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: "" }), + ); + }); + + 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", () => { diff --git a/src/telegram/bot/delivery.resolve-media.ts b/src/telegram/bot/delivery.resolve-media.ts index 50112236c90..e0f8d46abbd 100644 --- a/src/telegram/bot/delivery.resolve-media.ts +++ b/src/telegram/bot/delivery.resolve-media.ts @@ -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; }