From f558a240d1e60d33c84e2dd4e545120a4f2bb187 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Tue, 15 Sep 2026 18:01:39 -0700 Subject: [PATCH] Release failed asset responses and refuse cancelled requests --- src/director/packs/packs.test.mjs | 25 +++++++++++++++++++++++++ src/director/packs/source.js | 6 +++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/director/packs/packs.test.mjs b/src/director/packs/packs.test.mjs index 036f31c02..f31d65090 100644 --- a/src/director/packs/packs.test.mjs +++ b/src/director/packs/packs.test.mjs @@ -338,3 +338,28 @@ test('byte and integrity checks run before rendering; adapter names never resolv assert.equal(mounted, 1); session.destroy(); }); + +test('failed responses release their body and an already-cancelled source sends no request', async () => { + let cancelled = 0, + requests = 0; + const source = createAssetDirectorySource({ + baseUrl: 'https://example.org/packs/', + fetchImpl: async () => { + requests++; + return new Response( + new ReadableStream({ + cancel() { + cancelled++; + }, + }), + { status: 403 }, + ); + }, + }); + await assert.rejects(source({ path: 'a.json' }), /unavailable/); + assert.equal(cancelled, 1); + const controller = new AbortController(); + controller.abort(); + await assert.rejects(source({ path: 'a.json', signal: controller.signal })); + assert.equal(requests, 1); +}); diff --git a/src/director/packs/source.js b/src/director/packs/source.js index 50f4d2e53..4828827af 100644 --- a/src/director/packs/source.js +++ b/src/director/packs/source.js @@ -19,6 +19,7 @@ export function createAssetDirectorySource({ ); return async ({ path, signal, maxBytes = PACK_LIMITS.bytes }) => { validateAssetPath(path); + signal?.throwIfAborted(); const response = await fetchImpl(new URL(path, base).href, { signal, credentials: 'omit', @@ -26,7 +27,10 @@ export function createAssetDirectorySource({ referrerPolicy: 'no-referrer', cache: 'no-store', }); - if (!response.ok) throw new Error('Asset unavailable'); + if (!response.ok) { + await response.body?.cancel().catch(() => {}); + throw new Error('Asset unavailable'); + } const reader = response.body?.getReader(); if (!reader) throw new Error('Asset stream unavailable'); const chunks = [];