mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 07:17:29 +00:00
fix(telegram): handle large file getFile errors gracefully
Catch GrammyError when getFile fails for files >20MB (Telegram Bot API limit). Log warning, skip attachment, but continue processing message text. - Add FILE_TOO_BIG_RE regex to detect 'file is too big' errors - Add isFileTooBigError() and isRetryableGetFileError() helpers - Skip retrying permanent 400 errors (they'll fail every time) - Log specific warning for file size limit errors - Return null so message text is still processed Fixes #18518
This commit is contained in:
committed by
Peter Steinberger
parent
1953b938e3
commit
01b37f1d32
@@ -15,6 +15,7 @@ vi.mock("../../media/fetch.js", () => ({
|
||||
|
||||
vi.mock("../../globals.js", () => ({
|
||||
danger: (s: string) => s,
|
||||
warn: (s: string) => s,
|
||||
logVerbose: () => {},
|
||||
}));
|
||||
|
||||
@@ -134,4 +135,68 @@ describe("resolveMedia getFile retry", () => {
|
||||
expect(getFile).toHaveBeenCalledTimes(3);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("does not retry 'file is too big' error (400 Bad Request) and returns null", async () => {
|
||||
// Simulate Telegram Bot API error when file exceeds 20MB limit
|
||||
const fileTooBigError = new Error(
|
||||
"GrammyError: Call to 'getFile' failed! (400: Bad Request: file is too big)",
|
||||
);
|
||||
const getFile = vi.fn().mockRejectedValue(fileTooBigError);
|
||||
|
||||
const result = await resolveMedia(makeCtx("video", getFile), 10_000_000, "tok123");
|
||||
|
||||
// Should NOT retry - "file is too big" is a permanent error, not transient
|
||||
expect(getFile).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for audio when file is too big", async () => {
|
||||
const fileTooBigError = new Error(
|
||||
"GrammyError: Call to 'getFile' failed! (400: Bad Request: file is too big)",
|
||||
);
|
||||
const getFile = vi.fn().mockRejectedValue(fileTooBigError);
|
||||
|
||||
const result = await resolveMedia(makeCtx("audio", getFile), 10_000_000, "tok123");
|
||||
|
||||
expect(getFile).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for voice when file is too big", async () => {
|
||||
const fileTooBigError = new Error(
|
||||
"GrammyError: Call to 'getFile' failed! (400: Bad Request: file is too big)",
|
||||
);
|
||||
const getFile = vi.fn().mockRejectedValue(fileTooBigError);
|
||||
|
||||
const result = await resolveMedia(makeCtx("voice", getFile), 10_000_000, "tok123");
|
||||
|
||||
expect(getFile).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("still retries transient errors even after encountering file too big in different call", async () => {
|
||||
// First call with transient error should retry
|
||||
const getFile = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("Network request for 'getFile' failed!"))
|
||||
.mockResolvedValueOnce({ file_path: "voice/file_0.oga" });
|
||||
|
||||
fetchRemoteMedia.mockResolvedValueOnce({
|
||||
buffer: Buffer.from("audio"),
|
||||
contentType: "audio/ogg",
|
||||
fileName: "file_0.oga",
|
||||
});
|
||||
saveMediaBuffer.mockResolvedValueOnce({
|
||||
path: "/tmp/file_0.oga",
|
||||
contentType: "audio/ogg",
|
||||
});
|
||||
|
||||
const promise = resolveMedia(makeCtx("voice", getFile), 10_000_000, "tok123");
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
const result = await promise;
|
||||
|
||||
// Should retry transient errors
|
||||
expect(getFile).toHaveBeenCalledTimes(2);
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { RuntimeEnv } from "../../runtime.js";
|
||||
import type { TelegramInlineButtons } from "../button-types.js";
|
||||
import type { StickerMetadata, TelegramContext } from "./types.js";
|
||||
import { chunkMarkdownTextWithMode, type ChunkMode } from "../../auto-reply/chunk.js";
|
||||
import { danger, logVerbose } from "../../globals.js";
|
||||
import { danger, logVerbose, warn } from "../../globals.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { retryAsync } from "../../infra/retry.js";
|
||||
import { mediaKindFromMime } from "../../media/constants.js";
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
|
||||
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
|
||||
const VOICE_FORBIDDEN_RE = /VOICE_MESSAGES_FORBIDDEN/;
|
||||
const FILE_TOO_BIG_RE = /file is too big/i;
|
||||
|
||||
export async function deliverReplies(params: {
|
||||
replies: ReplyPayload[];
|
||||
@@ -414,10 +415,20 @@ export async function resolveMedia(
|
||||
maxDelayMs: 4000,
|
||||
jitter: 0.2,
|
||||
label: "telegram:getFile",
|
||||
shouldRetry: isRetryableGetFileError,
|
||||
onRetry: ({ attempt, maxAttempts }) =>
|
||||
logVerbose(`telegram: getFile retry ${attempt}/${maxAttempts}`),
|
||||
});
|
||||
} catch (err) {
|
||||
// Handle "file is too big" separately - Telegram Bot API has a 20MB download limit
|
||||
if (isFileTooBigError(err)) {
|
||||
logVerbose(
|
||||
warn(
|
||||
"telegram: getFile failed - file exceeds Telegram Bot API 20MB limit; skipping attachment",
|
||||
),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
// All retries exhausted — return null so the message still reaches the agent
|
||||
// with a type-based placeholder (e.g. <media:audio>) instead of being dropped.
|
||||
logVerbose(`telegram: getFile failed after retries: ${String(err)}`);
|
||||
@@ -442,6 +453,31 @@ function isVoiceMessagesForbidden(err: unknown): boolean {
|
||||
return VOICE_FORBIDDEN_RE.test(formatErrorMessage(err));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the error is Telegram's "file is too big" error.
|
||||
* This happens when trying to download files >20MB via the Bot API.
|
||||
* Unlike network errors, this is a permanent error and should not be retried.
|
||||
*/
|
||||
function isFileTooBigError(err: unknown): boolean {
|
||||
if (err instanceof GrammyError) {
|
||||
return FILE_TOO_BIG_RE.test(err.description);
|
||||
}
|
||||
return FILE_TOO_BIG_RE.test(formatErrorMessage(err));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the error is a transient network error that should be retried.
|
||||
* Returns false for permanent errors like "file is too big" (400 Bad Request).
|
||||
*/
|
||||
function isRetryableGetFileError(err: unknown): boolean {
|
||||
// Don't retry "file is too big" - it's a permanent 400 error
|
||||
if (isFileTooBigError(err)) {
|
||||
return false;
|
||||
}
|
||||
// Retry all other errors (network issues, timeouts, etc.)
|
||||
return true;
|
||||
}
|
||||
|
||||
async function sendTelegramVoiceFallbackText(opts: {
|
||||
bot: Bot;
|
||||
chatId: string;
|
||||
|
||||
Reference in New Issue
Block a user