From 6972ef80b7056ef5191ae8eede2c3a38c8d0b7c6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 8 Aug 2026 16:44:44 -0700 Subject: [PATCH] fix(telegram): classify hosted media size errors consistently (#120757) --- .../telegram/src/bot-handlers.media.test.ts | 39 +++++++++++++++++++ extensions/telegram/src/bot-handlers.media.ts | 4 +- ...te-telegram-bot.channel-post-media.test.ts | 23 ++++++----- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/extensions/telegram/src/bot-handlers.media.test.ts b/extensions/telegram/src/bot-handlers.media.test.ts index 6e0daacc3a42..f911c2a0caa8 100644 --- a/extensions/telegram/src/bot-handlers.media.test.ts +++ b/extensions/telegram/src/bot-handlers.media.test.ts @@ -2,9 +2,48 @@ import { MediaFetchError } from "openclaw/plugin-sdk/media-runtime"; import { describe, expect, it } from "vitest"; import { isDurablyRetryableInboundMediaError, + isMediaSizeLimitError, isRecoverableMediaGroupError, + TelegramBotApiFileTooLargeError, } from "./bot-handlers.media.js"; +describe("isMediaSizeLimitError", () => { + it.each([ + { + name: "content-length limit", + error: new MediaFetchError("max_bytes", "content length 11 exceeds maxBytes 10"), + expected: true, + }, + { + name: "streaming payload limit", + error: new MediaFetchError("max_bytes", "payload exceeds maxBytes 10"), + expected: true, + }, + { + name: "Telegram Bot API file limit", + error: new TelegramBotApiFileTooLargeError(new Error("Bad Request: file is too big")), + expected: true, + }, + { + name: "legacy untyped size error", + error: new Error("media exceeds 20 MB limit"), + expected: true, + }, + { + name: "fetch failure with misleading size text", + error: new MediaFetchError("fetch_failed", "media exceeds 20 MB limit"), + expected: false, + }, + { + name: "HTTP failure with misleading size text", + error: new MediaFetchError("http_error", "media exceeds 20 MB limit", { status: 500 }), + expected: false, + }, + ])("classifies $name by its authoritative error code", ({ error, expected }) => { + expect(isMediaSizeLimitError(error)).toBe(expected); + }); +}); + describe("isDurablyRetryableInboundMediaError", () => { const networkCause = () => Object.assign(new Error("read ECONNRESET"), { code: "ECONNRESET" }); const abortCause = () => Object.assign(new Error("aborted"), { name: "AbortError" }); diff --git a/extensions/telegram/src/bot-handlers.media.ts b/extensions/telegram/src/bot-handlers.media.ts index 45647bda20ac..07179eb450fb 100644 --- a/extensions/telegram/src/bot-handlers.media.ts +++ b/extensions/telegram/src/bot-handlers.media.ts @@ -19,8 +19,8 @@ export class TelegramBotApiFileTooLargeError extends MediaFetchError { } export function isMediaSizeLimitError(err: unknown): boolean { - if (err instanceof TelegramBotApiFileTooLargeError) { - return true; + if (err instanceof MediaFetchError) { + return err.code === "max_bytes"; } const errMsg = String(err); return errMsg.includes("exceeds") && errMsg.includes("MB limit"); diff --git a/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts b/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts index e5a6cb90dfbe..f7534d22d318 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.channel-post-media.test.ts @@ -420,7 +420,7 @@ describe("createTelegramBot channel_post media", () => { const response = await params.fetchImpl(params.url); const buffer = new Uint8Array(await response.arrayBuffer()); if (buffer.length > params.maxBytes) { - throw new MediaFetchError("max_bytes", `media exceeds ${params.maxBytes} MB limit`); + throw new MediaFetchError("max_bytes", `payload exceeds maxBytes ${params.maxBytes}`); } return { path: "/tmp/telegram-media.bin", @@ -507,7 +507,6 @@ describe("createTelegramBot channel_post media", () => { it("dispatches an oversized channel_post as a type-only media fact", async () => { setOpenChannelPostConfig(); - const fetchSpy = createImageFetchSpy({ body: new Uint8Array([0xff, 0xd8, 0xff, 0x00]), contentType: "image/jpeg", @@ -525,6 +524,7 @@ describe("createTelegramBot channel_post media", () => { ); expect(replySpy).toHaveBeenCalledOnce(); + expect(sendMessageSpy).not.toHaveBeenCalled(); expectTypeOnlyMediaPayload("image"); fetchSpy.mockRestore(); }); @@ -608,21 +608,21 @@ describe("createTelegramBot channel_post media", () => { cause: Object.assign(new Error("aborted"), { name: "AbortError" }), }), result: { kind: "failed-retryable", error: expect.any(MediaFetchError) }, - warnings: 0, + warning: undefined, }, { name: "permanent oversized media", messageId: 98077, error: new MediaFetchError("max_bytes", "Failed to fetch media: payload exceeds maxBytes 10"), result: { kind: "completed" }, - warnings: 1, + warning: "⚠️ File too large. Maximum size is 100MB.", }, { name: "permanent SSRF rejection", messageId: 98078, error: new MediaFetchError("fetch_failed", "blocked by SSRF guard: private address"), result: { kind: "completed" }, - warnings: 1, + warning: "⚠️ Failed to download media. Please try again.", }, ])("preserves durable replay handling for $name (#98076)", async (testCase) => { setOpenTelegramDirectConfig(); @@ -640,13 +640,12 @@ describe("createTelegramBot channel_post media", () => { withTelegramSpooledReplayUpdate(update, () => handler(ctx)), ); expect(result).toEqual(testCase.result); - expect(sendMessageSpy).toHaveBeenCalledTimes(testCase.warnings); - expect(replySpy).toHaveBeenCalledTimes(testCase.warnings); - expect(sendMessageSpy.mock.calls[0]?.[1]).toEqual( - [undefined, "⚠️ Failed to download media. Please try again."][testCase.warnings], - ); - if (testCase.warnings) { - expectTelegramDownloadWarning(testCase.messageId); + const expectedWarnings = testCase.warning ? 1 : 0; + expect(sendMessageSpy).toHaveBeenCalledTimes(expectedWarnings); + expect(replySpy).toHaveBeenCalledTimes(expectedWarnings); + expect(sendMessageSpy.mock.calls[0]?.[1]).toBe(testCase.warning); + if (testCase.warning) { + expectTelegramDownloadWarning(testCase.messageId, testCase.warning); expectTypeOnlyMediaPayload("document"); } });