Files
openclaw/src/media/fetch.test.ts
Frank Yang 7a53eb7ea8 fix: retry Telegram inbound media downloads over IPv4 fallback (#45327)
* fix: retry telegram inbound media downloads over ipv4

* fix: preserve telegram media retry errors

* fix: redact telegram media fetch errors
2026-03-14 08:21:31 +08:00

152 lines
4.6 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { fetchRemoteMedia } from "./fetch.js";
function makeStream(chunks: Uint8Array[]) {
return new ReadableStream<Uint8Array>({
start(controller) {
for (const chunk of chunks) {
controller.enqueue(chunk);
}
controller.close();
},
});
}
function makeStallingFetch(firstChunk: Uint8Array) {
return vi.fn(async () => {
return new Response(
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(firstChunk);
},
}),
{ status: 200 },
);
});
}
function makeLookupFn() {
return vi.fn(async () => [{ address: "149.154.167.220", family: 4 }]) as unknown as NonNullable<
Parameters<typeof fetchRemoteMedia>[0]["lookupFn"]
>;
}
describe("fetchRemoteMedia", () => {
const telegramToken = "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZabcd";
const redactedTelegramToken = `${telegramToken.slice(0, 6)}${telegramToken.slice(-4)}`;
const telegramFileUrl = `https://api.telegram.org/file/bot${telegramToken}/photos/1.jpg`;
it("rejects when content-length exceeds maxBytes", async () => {
const lookupFn = vi.fn(async () => [
{ address: "93.184.216.34", family: 4 },
]) as unknown as NonNullable<Parameters<typeof fetchRemoteMedia>[0]["lookupFn"]>;
const fetchImpl = async () =>
new Response(makeStream([new Uint8Array([1, 2, 3, 4, 5])]), {
status: 200,
headers: { "content-length": "5" },
});
await expect(
fetchRemoteMedia({
url: "https://example.com/file.bin",
fetchImpl,
maxBytes: 4,
lookupFn,
}),
).rejects.toThrow("exceeds maxBytes");
});
it("rejects when streamed payload exceeds maxBytes", async () => {
const lookupFn = vi.fn(async () => [
{ address: "93.184.216.34", family: 4 },
]) as unknown as NonNullable<Parameters<typeof fetchRemoteMedia>[0]["lookupFn"]>;
const fetchImpl = async () =>
new Response(makeStream([new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6])]), {
status: 200,
});
await expect(
fetchRemoteMedia({
url: "https://example.com/file.bin",
fetchImpl,
maxBytes: 4,
lookupFn,
}),
).rejects.toThrow("exceeds maxBytes");
});
it("aborts stalled body reads when idle timeout expires", async () => {
const lookupFn = vi.fn(async () => [
{ address: "93.184.216.34", family: 4 },
]) as unknown as NonNullable<Parameters<typeof fetchRemoteMedia>[0]["lookupFn"]>;
const fetchImpl = makeStallingFetch(new Uint8Array([1, 2]));
await expect(
fetchRemoteMedia({
url: "https://example.com/file.bin",
fetchImpl,
lookupFn,
maxBytes: 1024,
readIdleTimeoutMs: 20,
}),
).rejects.toMatchObject({
code: "fetch_failed",
name: "MediaFetchError",
});
}, 5_000);
it("redacts Telegram bot tokens from fetch failure messages", async () => {
const fetchImpl = vi.fn(async () => {
throw new Error(`dial failed for ${telegramFileUrl}`);
});
const error = await fetchRemoteMedia({
url: telegramFileUrl,
fetchImpl,
lookupFn: makeLookupFn(),
maxBytes: 1024,
ssrfPolicy: {
allowedHostnames: ["api.telegram.org"],
allowRfc2544BenchmarkRange: true,
},
}).catch((err: unknown) => err as Error);
expect(error).toBeInstanceOf(Error);
const errorText = error instanceof Error ? String(error) : "";
expect(errorText).not.toContain(telegramToken);
expect(errorText).toContain(`bot${redactedTelegramToken}`);
});
it("redacts Telegram bot tokens from HTTP error messages", async () => {
const fetchImpl = vi.fn(async () => new Response("unauthorized", { status: 401 }));
const error = await fetchRemoteMedia({
url: telegramFileUrl,
fetchImpl,
lookupFn: makeLookupFn(),
maxBytes: 1024,
ssrfPolicy: {
allowedHostnames: ["api.telegram.org"],
allowRfc2544BenchmarkRange: true,
},
}).catch((err: unknown) => err as Error);
expect(error).toBeInstanceOf(Error);
const errorText = error instanceof Error ? String(error) : "";
expect(errorText).not.toContain(telegramToken);
expect(errorText).toContain(`bot${redactedTelegramToken}`);
});
it("blocks private IP literals before fetching", async () => {
const fetchImpl = vi.fn();
await expect(
fetchRemoteMedia({
url: "http://127.0.0.1/secret.jpg",
fetchImpl,
maxBytes: 1024,
}),
).rejects.toThrow(/private|internal|blocked/i);
expect(fetchImpl).not.toHaveBeenCalled();
});
});