From d74f65b06faa00596d80aa6960c7d3af232beac5 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 4 Oct 2026 12:31:25 +0800 Subject: [PATCH] fix(fetch): reject locked bodies before consumption and cloning --- .../tests/fixtures/runtime/locked_body.js | 47 +++++++++++++++++++ .../tests/fixtures/runtime/web_streams.html | 14 +++++- moli-core/tests/web_apis.rs | 3 ++ moli-core/tests/web_apis/locked_body.rs | 22 +++++++++ moli-renderer-v8/src/context_bootstrap.rs | 3 +- .../src/context_bootstrap/stream_adapter.rs | 5 +- .../stream_adapter/readable_state.rs | 2 +- moli-renderer-v8/src/network_host.rs | 4 +- moli-renderer-v8/src/network_host/body.rs | 21 +++++++++ .../src/network_host/fetch_surface.rs | 10 +++- .../src/network_host/response/body_methods.rs | 5 ++ 11 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 moli-core/tests/fixtures/runtime/locked_body.js create mode 100644 moli-core/tests/web_apis/locked_body.rs diff --git a/moli-core/tests/fixtures/runtime/locked_body.js b/moli-core/tests/fixtures/runtime/locked_body.js new file mode 100644 index 0000000000..2cb4b80fe7 --- /dev/null +++ b/moli-core/tests/fixtures/runtime/locked_body.js @@ -0,0 +1,47 @@ +async function runLockedBodyProbe(url) { + const errors = []; + const check = (value, label) => { if (!value) errors.push(label); }; + const throwsTypeError = (callback, label) => { + try { callback(); errors.push(label + ': accepted'); } + catch (error) { check(error instanceof TypeError, label + ': wrong error ' + error); } + }; + const rejectsTypeError = async (callback, label) => { + let promise; + try { promise = callback(); } + catch (error) { errors.push(label + ': synchronous throw ' + error); return; } + try { await promise; errors.push(label + ': fulfilled'); } + catch (error) { check(error instanceof TypeError, label + ': wrong rejection ' + error); } + }; + const methods = ['text', 'json', 'arrayBuffer', 'bytes', 'blob', 'formData']; + const streamFrom = value => new ReadableStream({ + start(controller) { controller.enqueue(new TextEncoder().encode(value)); controller.close(); } + }); + const makeBody = async (source, method = 'text') => { + const text = method === 'formData' ? 'name=value' : '{"value":1}'; + const headers = {'Content-Type': method === 'formData' + ? 'application/x-www-form-urlencoded' : 'application/json'}; + if (source === 'request') return new Request(url, {method: 'POST', body: text, headers}); + if (source === 'response') return new Response(text, {headers}); + if (source === 'stream') return new Response(streamFrom(text), {headers}); + return fetch(url); + }; + const sources = ['request', 'response', 'stream', 'fetch']; + + for (const source of sources) { + for (const method of methods) { + const body = await makeBody(source, method); + const reader = body.body.getReader(); + Object.defineProperty(body.body, 'locked', {get() { + throw new Error('public locked getter consulted'); + }}); + check(!body.bodyUsed, source + ': locking alone does not disturb'); + throwsTypeError(() => body.clone(), source + ': locked clone'); + await rejectsTypeError(() => body[method](), source + ': locked ' + method); + check(!body.bodyUsed, source + ': failed ' + method + ' must not disturb'); + reader.releaseLock(); + try { await body.text(); } + catch (error) { errors.push(source + ': released unused body cannot be consumed ' + error); } + } + } + return {errors}; +} diff --git a/moli-core/tests/fixtures/runtime/web_streams.html b/moli-core/tests/fixtures/runtime/web_streams.html index b31e1d0c01..20fcf0c80c 100644 --- a/moli-core/tests/fixtures/runtime/web_streams.html +++ b/moli-core/tests/fixtures/runtime/web_streams.html @@ -11,6 +11,7 @@