diff --git a/extensions/telegram/src/bot/delivery.resolve-media-retry.test.ts b/extensions/telegram/src/bot/delivery.resolve-media-retry.test.ts index f54d62abe67..e306ef2a605 100644 --- a/extensions/telegram/src/bot/delivery.resolve-media-retry.test.ts +++ b/extensions/telegram/src/bot/delivery.resolve-media-retry.test.ts @@ -239,6 +239,31 @@ describe("resolveMedia getFile retry", () => { expect(fetchRemoteMedia).toHaveBeenCalledTimes(1); }); + it("retries fetchRemoteMedia on transient HTTP 5xx error and succeeds", async () => { + const getFile = vi.fn().mockResolvedValue({ file_path: "voice/file_0.oga" }); + fetchRemoteMedia + .mockRejectedValueOnce(new MockMediaFetchError("http_error", "HTTP 502 Bad Gateway")) + .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), MAX_MEDIA_BYTES, BOT_TOKEN); + await flushRetryTimers(); + const result = await promise; + + expect(getFile).toHaveBeenCalledTimes(1); + expect(fetchRemoteMedia).toHaveBeenCalledTimes(2); + expect(result).toEqual( + expect.objectContaining({ path: "/tmp/file_0.oga", placeholder: "" }), + ); + }); + it("does not retry fetchRemoteMedia on max_bytes policy violation", async () => { const getFile = vi.fn().mockResolvedValue({ file_path: "video/large.mp4" }); fetchRemoteMedia.mockRejectedValueOnce( diff --git a/extensions/telegram/src/bot/delivery.resolve-media.ts b/extensions/telegram/src/bot/delivery.resolve-media.ts index d096a1beb05..e0d744be096 100644 --- a/extensions/telegram/src/bot/delivery.resolve-media.ts +++ b/extensions/telegram/src/bot/delivery.resolve-media.ts @@ -66,9 +66,19 @@ function isRetryableGetFileError(err: unknown): boolean { */ function isRetryableDownloadError(err: unknown): boolean { if (err instanceof MediaFetchError) { - // Only retry transient fetch failures (network issues, timeouts, connection drops). - // Do not retry HTTP errors (4xx, 5xx) or policy violations (max_bytes). - return err.code === "fetch_failed"; + // Retry transient fetch failures (network issues, timeouts, connection drops). + if (err.code === "fetch_failed") { + return true; + } + // Retry transient HTTP 5xx server errors; do not retry 4xx or policy violations. + if (err.code === "http_error") { + const match = /HTTP (\d{3})/.exec(err.message); + if (match) { + const status = Number(match[1]); + return status >= 500; + } + } + return false; } // For non-MediaFetchError, check if it looks like a transient network error. const msg = formatErrorMessage(err).toLowerCase();