

























@@ -316,6 +316,167 @@ describe("readRemoteMediaBuffer", () => {
316316});
317317});
318318319+it("retries transient fetch failures when retry is enabled", async () => {
320+const transientError = Object.assign(new Error("socket hang up"), { code: "ECONNRESET" });
321+const fetchImpl = vi
322+.fn()
323+.mockRejectedValueOnce(transientError)
324+.mockResolvedValueOnce(new Response("ok", { status: 200 }));
325+326+const result = await readRemoteMediaBuffer({
327+url: "https://example.com/file.bin",
328+ fetchImpl,
329+lookupFn: makeLookupFn(),
330+maxBytes: 1024,
331+retry: { attempts: 3, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
332+});
333+334+expect(result.buffer.toString()).toBe("ok");
335+expect(fetchImpl).toHaveBeenCalledTimes(2);
336+});
337+338+it("retries 5xx responses when retry is enabled", async () => {
339+const fetchImpl = vi
340+.fn()
341+.mockResolvedValueOnce(
342+new Response("busy", { status: 503, statusText: "Service Unavailable" }),
343+)
344+.mockResolvedValueOnce(new Response("ok", { status: 200 }));
345+346+const result = await readRemoteMediaBuffer({
347+url: "https://example.com/file.bin",
348+ fetchImpl,
349+lookupFn: makeLookupFn(),
350+maxBytes: 1024,
351+retry: { attempts: 3, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
352+});
353+354+expect(result.buffer.toString()).toBe("ok");
355+expect(fetchImpl).toHaveBeenCalledTimes(2);
356+});
357+358+it("retries 408 responses when retry is enabled", async () => {
359+const fetchImpl = vi
360+.fn()
361+.mockResolvedValueOnce(
362+new Response("timeout", { status: 408, statusText: "Request Timeout" }),
363+)
364+.mockResolvedValueOnce(new Response("ok", { status: 200 }));
365+366+const result = await readRemoteMediaBuffer({
367+url: "https://example.com/file.bin",
368+ fetchImpl,
369+lookupFn: makeLookupFn(),
370+maxBytes: 1024,
371+retry: { attempts: 3, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
372+});
373+374+expect(result.buffer.toString()).toBe("ok");
375+expect(fetchImpl).toHaveBeenCalledTimes(2);
376+});
377+378+it("retries transient response body read failures when retry is enabled", async () => {
379+const transientError = Object.assign(new Error("socket hang up"), { code: "ECONNRESET" });
380+const fetchImpl = vi
381+.fn()
382+.mockResolvedValueOnce(
383+new Response(
384+new ReadableStream<Uint8Array>({
385+start(controller) {
386+controller.error(transientError);
387+},
388+}),
389+{ status: 200 },
390+),
391+)
392+.mockResolvedValueOnce(new Response("ok", { status: 200 }));
393+394+const result = await readRemoteMediaBuffer({
395+url: "https://example.com/file.bin",
396+ fetchImpl,
397+lookupFn: makeLookupFn(),
398+maxBytes: 1024,
399+retry: { attempts: 3, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
400+});
401+402+expect(result.buffer.toString()).toBe("ok");
403+expect(fetchImpl).toHaveBeenCalledTimes(2);
404+});
405+406+it("does not retry 4xx responses", async () => {
407+const fetchImpl = vi
408+.fn()
409+.mockResolvedValueOnce(new Response("missing", { status: 404 }))
410+.mockResolvedValueOnce(new Response("ok", { status: 200 }));
411+412+await expect(
413+readRemoteMediaBuffer({
414+url: "https://example.com/file.bin",
415+ fetchImpl,
416+lookupFn: makeLookupFn(),
417+maxBytes: 1024,
418+retry: { attempts: 3, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
419+}),
420+).rejects.toMatchObject({ code: "http_error", status: 404 });
421+expect(fetchImpl).toHaveBeenCalledTimes(1);
422+});
423+424+it("does not retry caller aborts", async () => {
425+const abortError = new Error("This operation was aborted");
426+abortError.name = "AbortError";
427+const fetchImpl = vi
428+.fn()
429+.mockRejectedValueOnce(abortError)
430+.mockResolvedValueOnce(new Response("ok", { status: 200 }));
431+432+await expect(
433+readRemoteMediaBuffer({
434+url: "https://example.com/file.bin",
435+ fetchImpl,
436+lookupFn: makeLookupFn(),
437+maxBytes: 1024,
438+retry: { attempts: 3, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
439+}),
440+).rejects.toMatchObject({ code: "fetch_failed" });
441+expect(fetchImpl).toHaveBeenCalledTimes(1);
442+});
443+444+it("does not retry SSRF guard blocks", async () => {
445+const fetchImpl = vi.fn();
446+447+await expect(
448+readRemoteMediaBuffer({
449+url: "http://127.0.0.1/secret.jpg",
450+ fetchImpl,
451+lookupFn: makeLookupFn(),
452+maxBytes: 1024,
453+retry: { attempts: 3, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
454+}),
455+).rejects.toThrow(/private|internal|blocked/i);
456+expect(fetchWithSsrFGuardMock).toHaveBeenCalledTimes(1);
457+expect(fetchImpl).not.toHaveBeenCalled();
458+});
459+460+it("does not retry maxBytes failures", async () => {
461+const fetchImpl = vi
462+.fn()
463+.mockResolvedValueOnce(
464+new Response("large", { status: 200, headers: { "content-length": "5" } }),
465+)
466+.mockResolvedValueOnce(new Response("ok", { status: 200 }));
467+468+await expect(
469+readRemoteMediaBuffer({
470+url: "https://example.com/file.bin",
471+ fetchImpl,
472+lookupFn: makeLookupFn(),
473+maxBytes: 4,
474+retry: { attempts: 3, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
475+}),
476+).rejects.toMatchObject({ code: "max_bytes" });
477+expect(fetchImpl).toHaveBeenCalledTimes(1);
478+});
479+319480it.each([
320481{
321482name: "bounds error-body snippets instead of reading the full response",
@@ -552,9 +713,10 @@ describe("readRemoteMediaBuffer", () => {
552713});
553714554715it("retries saveRemoteMedia after a transient fetch failure", async () => {
716+const transientError = Object.assign(new TypeError("socket reset"), { code: "ECONNRESET" });
555717const fetchImpl = vi
556718.fn()
557-.mockRejectedValueOnce(new TypeError("socket reset"))
719+.mockRejectedValueOnce(transientError)
558720.mockResolvedValueOnce(
559721new Response(makeStream([new Uint8Array([5, 6])]), {
560722status: 200,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。