From e61462ac42a26f88a4dc10343dae411e4011ee6f Mon Sep 17 00:00:00 2001 From: vedanth Date: Mon, 26 Oct 2026 11:45:40 +0000 Subject: [PATCH 1/2] fix(media-server): validate each ranged download response --- .../__tests__/lib/parallel-download.test.ts | 96 +++++++++++++++++++ .../media-server/src/lib/parallel-download.ts | 23 +++++ 2 files changed, 119 insertions(+) diff --git a/apps/media-server/src/__tests__/lib/parallel-download.test.ts b/apps/media-server/src/__tests__/lib/parallel-download.test.ts index ce8290f87fd..abdee11f29d 100644 --- a/apps/media-server/src/__tests__/lib/parallel-download.test.ts +++ b/apps/media-server/src/__tests__/lib/parallel-download.test.ts @@ -50,6 +50,28 @@ beforeAll(async () => { if (url.pathname === "/changed" && Number(range[1]) > 0) { return new Response("precondition", { status: 412 }); } + if (url.pathname.startsWith("/mismatched-") && Number(range[1]) > 0) { + const start = Number(range[1]); + const end = Math.min(Number(range[2]), payload.length - 1); + const kind = url.pathname.slice("/mismatched-".length); + const contentRange = + kind === "missing" + ? undefined + : kind === "unknown-total" + ? `bytes ${start}-${end}/*` + : kind === "start" + ? `bytes 0-${end - start}/${payload.length}` + : kind === "end" + ? `bytes ${start}-${end + 1}/${payload.length}` + : `bytes ${start}-${end}/${payload.length + 1}`; + return new Response(payload.subarray(0, end - start + 1), { + status: 206, + headers: { + ETag: '"v1"', + ...(contentRange ? { "Content-Range": contentRange } : {}), + }, + }); + } const start = Number(range[1]); const end = Math.min(Number(range[2]), payload.length - 1); const body = @@ -167,6 +189,80 @@ describe("parallel download", () => { expect(rangeRequests).toBe(before + 1); }); + test.each(["start", "end", "total", "missing", "unknown-total"])( + "rejects a same-sized response with mismatched range metadata (%s)", + async (kind) => { + await expect( + downloadUrlToPathParallel( + `http://localhost:${server.port}/mismatched-${kind}`, + join(dir, `mismatched-${kind}.bin`), + { concurrency: 4, partBytes: MB }, + ), + ).rejects.toThrow("Content-Range"); + }, + ); + + test("does not write beyond a declared range before rejecting an oversized body", async () => { + const path = join(dir, "oversized.bin"); + const fetchImpl = Object.assign( + async () => + new Response(new Uint8Array(MB + 1), { + status: 206, + headers: { "Content-Range": `bytes 0-${MB - 1}/${MB}` }, + }), + { preconnect: fetch.preconnect }, + ); + await expect( + downloadUrlToPathParallel("https://media.test/file", path, { + concurrency: 2, + partBytes: MB, + fetchImpl, + }), + ).rejects.toThrow("expected"); + expect((await readFile(path)).length).toBeLessThanOrEqual(MB); + }); + + test("cancels an unread response whose range cannot be placed", async () => { + let cancelled = false; + let requests = 0; + const fetchImpl = Object.assign( + async () => { + requests++; + if (requests === 1) { + return new Response(new Uint8Array(MB), { + status: 206, + headers: { "Content-Range": `bytes 0-${MB - 1}/${2 * MB}` }, + }); + } + return new Response( + new ReadableStream({ + cancel() { + cancelled = true; + }, + }), + { + status: 206, + headers: { "Content-Range": `bytes 0-${MB - 1}/${2 * MB}` }, + }, + ); + }, + { preconnect: fetch.preconnect }, + ); + await expect( + downloadUrlToPathParallel( + "https://media.test/file", + join(dir, "cancelled-range.bin"), + { + concurrency: 2, + partBytes: MB, + fetchImpl, + }, + ), + ).rejects.toThrow("Content-Range"); + expect(cancelled).toBe(true); + expect(requests).toBe(2); + }); + test("fails when a part comes back short", async () => { await expect( downloadUrlToPathParallel( diff --git a/apps/media-server/src/lib/parallel-download.ts b/apps/media-server/src/lib/parallel-download.ts index f582f3537ba..c83a0822cef 100644 --- a/apps/media-server/src/lib/parallel-download.ts +++ b/apps/media-server/src/lib/parallel-download.ts @@ -80,9 +80,18 @@ async function readInto( while (true) { const { done, value } = await reader.read(); if (done) break; + const read = offset - start + value.byteLength; + if (expected !== null && read > expected) { + throw new Error( + `Ranged download returned more than expected ${expected} bytes`, + ); + } await write(value, offset); offset += value.byteLength; } + } catch (error) { + await reader.cancel(error).catch(() => {}); + throw error; } finally { reader.releaseLock(); } @@ -173,6 +182,20 @@ export async function downloadUrlToPathParallel( `Ranged download part failed: ${response.status} ${response.statusText}`, ); } + const receivedRange = parseContentRange( + response.headers.get("content-range"), + ); + if ( + receivedRange === null || + receivedRange.start !== part.start || + receivedRange.end !== part.end || + receivedRange.total !== total + ) { + await response.body?.cancel().catch(() => {}); + throw new Error( + "Ranged download part returned an unexpected Content-Range", + ); + } await readInto(response, write, part.start, part.end - part.start + 1); } }; From 9c25dadaf0d8f6eaa315cbbaa82d820680137ff5 Mon Sep 17 00:00:00 2001 From: vedanth Date: Mon, 26 Oct 2026 11:53:01 +0000 Subject: [PATCH 2/2] test(media-server): cover cumulative streamed range limits --- .../__tests__/lib/parallel-download.test.ts | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/apps/media-server/src/__tests__/lib/parallel-download.test.ts b/apps/media-server/src/__tests__/lib/parallel-download.test.ts index abdee11f29d..83038d71116 100644 --- a/apps/media-server/src/__tests__/lib/parallel-download.test.ts +++ b/apps/media-server/src/__tests__/lib/parallel-download.test.ts @@ -202,25 +202,45 @@ describe("parallel download", () => { }, ); - test("does not write beyond a declared range before rejecting an oversized body", async () => { - const path = join(dir, "oversized.bin"); - const fetchImpl = Object.assign( - async () => - new Response(new Uint8Array(MB + 1), { - status: 206, - headers: { "Content-Range": `bytes 0-${MB - 1}/${MB}` }, + test.each(["buffer", "stream"])( + "does not write beyond a declared range before rejecting an oversized %s", + async (mode) => { + const path = join(dir, `oversized-${mode}.bin`); + const chunks = [ + new Uint8Array(MB / 2), + new Uint8Array(MB / 2), + new Uint8Array(1), + ]; + const body = + mode === "buffer" + ? new Uint8Array(MB + 1) + : new ReadableStream({ + pull(controller) { + const chunk = chunks.shift(); + if (chunk) controller.enqueue(chunk); + else controller.close(); + }, + }); + const fetchImpl = Object.assign( + async () => + new Response(body, { + status: 206, + headers: { "Content-Range": `bytes 0-${MB - 1}/${MB}` }, + }), + { preconnect: fetch.preconnect }, + ); + await expect( + downloadUrlToPathParallel("https://media.test/file", path, { + concurrency: 2, + partBytes: MB, + fetchImpl, }), - { preconnect: fetch.preconnect }, - ); - await expect( - downloadUrlToPathParallel("https://media.test/file", path, { - concurrency: 2, - partBytes: MB, - fetchImpl, - }), - ).rejects.toThrow("expected"); - expect((await readFile(path)).length).toBeLessThanOrEqual(MB); - }); + ).rejects.toThrow("expected"); + const saved = await readFile(path); + expect(saved.length).toBeLessThanOrEqual(MB); + if (mode === "stream") expect(saved.length).toBe(MB); + }, + ); test("cancels an unread response whose range cannot be placed", async () => { let cancelled = false;