From 997c2eb1b48150f797aff016c850605db2e899c1 Mon Sep 17 00:00:00 2001 From: Rajdeep Singh Date: Sat, 14 Mar 2026 00:37:29 +0530 Subject: [PATCH 1/3] feat(fetch): Support Uint8Array / ArrayBuffer request body in Fetch RequestInit (#4957) --- core/runtime/src/fetch/mod.rs | 2 +- core/runtime/src/fetch/request.rs | 62 +++++++++++++++++++++++-- core/runtime/src/fetch/tests/request.rs | 45 ++++++++++++++++-- 3 files changed, 99 insertions(+), 10 deletions(-) diff --git a/core/runtime/src/fetch/mod.rs b/core/runtime/src/fetch/mod.rs index 69cabf3c67c..befe4b7d242 100644 --- a/core/runtime/src/fetch/mod.rs +++ b/core/runtime/src/fetch/mod.rs @@ -146,7 +146,7 @@ async fn fetch_inner( }; let mut request = if let Some(options) = options { - options.into_request_builder(Some(request))? + options.into_request_builder(Some(request), &mut context.borrow_mut())? } else { request }; diff --git a/core/runtime/src/fetch/request.rs b/core/runtime/src/fetch/request.rs index f09c93530af..03dbf564fe2 100644 --- a/core/runtime/src/fetch/request.rs +++ b/core/runtime/src/fetch/request.rs @@ -4,6 +4,7 @@ //! //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request use super::HttpRequest; +use boa_engine::object::builtins::{JsArrayBuffer, JsDataView, JsTypedArray}; use boa_engine::value::{Convert, TryFromJs}; use boa_engine::{ Finalize, JsData, JsObject, JsResult, JsString, JsValue, Trace, boa_class, js_error, @@ -70,6 +71,7 @@ impl RequestInit { pub fn into_request_builder( mut self, request: Option>>, + context: &mut boa_engine::Context, ) -> JsResult>> { let mut builder = HttpRequest::builder(); let mut request_body = Vec::new(); @@ -105,11 +107,59 @@ impl RequestInit { if let Some(body) = &self.body { // TODO: add more support types. - if let Some(body) = body.as_string() { - let body = body.to_std_string().map_err( + if let Some(str_body) = body.as_string() { + let body_str = str_body.to_std_string().map_err( |_| js_error!(TypeError: "Request constructor: body is not a valid string"), )?; - request_body = body.into_bytes(); + request_body = body_str.into_bytes(); + } else if let Some(object) = body.as_object() { + if let Ok(buf) = JsArrayBuffer::from_object(object.clone()) { + if let Some(bytes) = buf.to_vec() { + request_body = bytes; + } else { + return Err( + js_error!(TypeError: "Request constructor: ArrayBuffer detached or unusable"), + ); + } + } else if let Ok(ta) = JsTypedArray::from_object(object.clone()) { + let buffer = ta.buffer(context)?; + let array_buffer = JsArrayBuffer::from_object( + buffer + .as_object() + .expect("buffer must be an object") + .clone(), + )?; + if let Some(bytes) = array_buffer.to_vec() { + let offset = ta.byte_offset(context)?; + let length = ta.byte_length(context)?; + request_body = bytes[offset..offset + length].to_vec(); + } else { + return Err( + js_error!(TypeError: "Request constructor: TypedArray buffer detached or unusable"), + ); + } + } else if let Ok(dv) = JsDataView::from_object(object.clone()) { + let buffer = dv.buffer(context)?; + let array_buffer = JsArrayBuffer::from_object( + buffer + .as_object() + .expect("buffer must be an object") + .clone(), + )?; + if let Some(bytes) = array_buffer.to_vec() { + let offset = dv.byte_offset(context)? as usize; + let length = dv.byte_length(context)? as usize; + request_body = bytes[offset..offset + length].to_vec(); + } else { + return Err( + js_error!(TypeError: "Request constructor: DataView buffer detached or unusable"), + ); + } + } else { + return Err( + js_error!(TypeError: "Request constructor: body is not a supported type"), + ); + } } else { return Err( js_error!(TypeError: "Request constructor: body is not a supported type"), @@ -158,6 +208,7 @@ impl JsRequest { pub fn create_from_js( input: Either, options: Option, + context: &mut boa_engine::Context, ) -> JsResult { let request = match input { Either::Left(uri) => { @@ -175,7 +226,7 @@ impl JsRequest { }; if let Some(options) = options { - let inner = options.into_request_builder(Some(request))?; + let inner = options.into_request_builder(Some(request), context)?; Ok(Self { inner }) } else { Ok(Self { inner: request }) @@ -199,6 +250,7 @@ impl JsRequest { pub fn constructor( input: Either, options: Option, + context: &mut boa_engine::Context, ) -> JsResult { // Need to use a match as `Either::map_right` does not have an equivalent // `Either::map_right_ok`. @@ -212,6 +264,6 @@ impl JsRequest { } Either::Left(i) => Either::Left(i), }; - JsRequest::create_from_js(input, options) + JsRequest::create_from_js(input, options, context) } } diff --git a/core/runtime/src/fetch/tests/request.rs b/core/runtime/src/fetch/tests/request.rs index 320bdd98c24..7429bd4d87a 100644 --- a/core/runtime/src/fetch/tests/request.rs +++ b/core/runtime/src/fetch/tests/request.rs @@ -39,10 +39,13 @@ fn request_constructor() { "Hello World".as_bytes() ); }), - TestAction::inspect_context(|_ctx| { - let request = - JsRequest::create_from_js(Either::Left(js_string!("http://example.com")), None) - .unwrap(); + TestAction::inspect_context(|ctx| { + let request = JsRequest::create_from_js( + Either::Left(js_string!("http://example.com")), + None, + ctx, + ) + .unwrap(); assert_eq!(request.uri().to_string(), "http://example.com/"); }), ]); @@ -152,3 +155,37 @@ fn request_clone_no_body_preserved() { }), ]); } +#[test] +fn request_body_typedarray() { + run_test_actions([ + TestAction::inspect_context(|ctx| { + let fetcher = TestFetcher::default(); + crate::fetch::register(fetcher, None, ctx).expect("failed to register fetch"); + }), + TestAction::run( + r#" + const buf = new Uint8Array([104, 101, 108, 108, 111]); // "hello" + globalThis.req1 = new Request("http://unit.test", { + method: "POST", + body: buf, + }); + const dv = new DataView(buf.buffer); + globalThis.req2 = new Request("http://unit.test", { + method: "POST", + body: dv, + }); + "#, + ), + TestAction::inspect_context(|ctx| { + let request1 = ctx.global_object().get(js_str!("req1"), ctx).unwrap(); + let request1_obj = request1.as_object().unwrap(); + let request1 = request1_obj.downcast_ref::().unwrap(); + assert_eq!(request1.inner().body().as_slice(), b"hello"); + + let request2 = ctx.global_object().get(js_str!("req2"), ctx).unwrap(); + let request2_obj = request2.as_object().unwrap(); + let request2 = request2_obj.downcast_ref::().unwrap(); + assert_eq!(request2.inner().body().as_slice(), b"hello"); + }), + ]); +} From def8ffa3a11f9eed6ef9d8c2bf6e15d8d42d3e3e Mon Sep 17 00:00:00 2001 From: Rajdeep Singh Date: Sat, 14 Mar 2026 00:47:15 +0530 Subject: [PATCH 2/3] ci: fix required toolchain input for dtolnay/rust-toolchain action --- .github/workflows/nightly_build.yml | 2 +- .github/workflows/pull_request.yml | 2 +- .github/workflows/release.yml | 6 +++--- .github/workflows/rust.yml | 21 ++++++++++++--------- .github/workflows/test262_pr.yml | 2 +- .github/workflows/test262_release.yml | 2 +- .github/workflows/webassembly.yml | 2 +- 7 files changed, 20 insertions(+), 17 deletions(-) diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index 338fdcb8b40..997f2defda8 100644 --- a/.github/workflows/nightly_build.yml +++ b/.github/workflows/nightly_build.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable targets: ${{ matrix.target }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 55daffbda1d..04e8fa93692 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -23,7 +23,7 @@ jobs: fetch-depth: 0 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6da607c9ea1..397c5681fad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -52,7 +52,7 @@ jobs: uses: actions/checkout@v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable targets: wasm32-unknown-unknown @@ -112,7 +112,7 @@ jobs: uses: actions/checkout@v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable targets: ${{ matrix.target }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b7723f3b49c..dff34c83f50 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -37,7 +37,9 @@ jobs: uses: actions/checkout@v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable - name: Cache cargo uses: actions/cache@v5 @@ -82,7 +84,7 @@ jobs: uses: actions/checkout@v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -128,8 +130,9 @@ jobs: uses: actions/checkout@v6 - name: Install Rust nightly with miri - uses: dtolnay/rust-toolchain@nightly + uses: dtolnay/rust-toolchain@master with: + toolchain: nightly components: miri - name: Cache cargo @@ -165,7 +168,7 @@ jobs: run: echo "rust_version=$(grep '^rust-version' Cargo.toml | cut -d' ' -f3 | tr -d '"')" >> $GITHUB_OUTPUT - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: ${{ steps.rust_version.outputs.rust_version }} @@ -185,7 +188,7 @@ jobs: uses: actions/checkout@v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable components: rustfmt @@ -217,7 +220,7 @@ jobs: uses: actions/checkout@v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable components: clippy @@ -260,7 +263,7 @@ jobs: uses: actions/checkout@v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -289,7 +292,7 @@ jobs: uses: actions/checkout@v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -321,7 +324,7 @@ jobs: uses: actions/checkout@v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable diff --git a/.github/workflows/test262_pr.yml b/.github/workflows/test262_pr.yml index b82550c5ad5..6d84783d75f 100644 --- a/.github/workflows/test262_pr.yml +++ b/.github/workflows/test262_pr.yml @@ -30,7 +30,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable diff --git a/.github/workflows/test262_release.yml b/.github/workflows/test262_release.yml index 9587ea28719..f7dc2da8af7 100644 --- a/.github/workflows/test262_release.yml +++ b/.github/workflows/test262_release.yml @@ -26,7 +26,7 @@ jobs: # Install Rust toolchain - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable diff --git a/.github/workflows/webassembly.yml b/.github/workflows/webassembly.yml index 7c3df11d646..28ddc00e0f2 100644 --- a/.github/workflows/webassembly.yml +++ b/.github/workflows/webassembly.yml @@ -45,7 +45,7 @@ jobs: uses: actions/checkout@v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: toolchain: stable From a6b5eb9805da203e9fe9ad56b71c72426249db23 Mon Sep 17 00:00:00 2001 From: Rajdeep Singh Date: Sat, 14 Mar 2026 01:07:32 +0530 Subject: [PATCH 3/3] refactor(fetch): optimize array buffer extraction and resolve PR feedback --- core/engine/src/module/loader/mod.rs | 5 +- core/runtime/src/fetch/request.rs | 71 +++++++++++++++++-------- core/runtime/src/fetch/tests/request.rs | 33 ++++++++++++ 3 files changed, 86 insertions(+), 23 deletions(-) diff --git a/core/engine/src/module/loader/mod.rs b/core/engine/src/module/loader/mod.rs index c38f7b708d7..67c10752321 100644 --- a/core/engine/src/module/loader/mod.rs +++ b/core/engine/src/module/loader/mod.rs @@ -62,7 +62,10 @@ pub fn resolve_module_specifier( // On Windows, also replace `/` with `\`. JavaScript imports use `/` as path separator. #[cfg(target_family = "windows")] - let specifier = specifier.replace('/', "\\"); + let specifier = { + use cow_utils::CowUtils; + specifier.cow_replace('/', "\\").into_owned() + }; let short_path = Path::new(&specifier); diff --git a/core/runtime/src/fetch/request.rs b/core/runtime/src/fetch/request.rs index 03dbf564fe2..b2365480ce2 100644 --- a/core/runtime/src/fetch/request.rs +++ b/core/runtime/src/fetch/request.rs @@ -4,7 +4,7 @@ //! //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request use super::HttpRequest; -use boa_engine::object::builtins::{JsArrayBuffer, JsDataView, JsTypedArray}; +use boa_engine::object::builtins::{JsArrayBuffer, JsDataView, JsSharedArrayBuffer, JsTypedArray}; use boa_engine::value::{Convert, TryFromJs}; use boa_engine::{ Finalize, JsData, JsObject, JsResult, JsString, JsValue, Trace, boa_class, js_error, @@ -68,6 +68,7 @@ impl RequestInit { /// /// # Errors /// If the body is not a valid type, an error is returned. + #[allow(clippy::too_many_lines)] pub fn into_request_builder( mut self, request: Option>>, @@ -106,7 +107,7 @@ impl RequestInit { } if let Some(body) = &self.body { - // TODO: add more support types. + // TODO: support additional Fetch body types (e.g., Blob, FormData, URLSearchParams). if let Some(str_body) = body.as_string() { let body_str = str_body.to_std_string().map_err( |_| js_error!(TypeError: "Request constructor: body is not a valid string"), @@ -121,38 +122,64 @@ impl RequestInit { js_error!(TypeError: "Request constructor: ArrayBuffer detached or unusable"), ); } + } else if let Ok(buf) = JsSharedArrayBuffer::from_object(object.clone()) { + request_body = buf.to_vec(); } else if let Ok(ta) = JsTypedArray::from_object(object.clone()) { let buffer = ta.buffer(context)?; - let array_buffer = JsArrayBuffer::from_object( - buffer - .as_object() - .expect("buffer must be an object") - .clone(), - )?; - if let Some(bytes) = array_buffer.to_vec() { - let offset = ta.byte_offset(context)?; - let length = ta.byte_length(context)?; + let buffer_obj = buffer + .as_object() + .ok_or_else(|| { + js_error!(TypeError: "Request constructor: TypedArray buffer must be an object") + })? + .clone(); + + let offset = ta.byte_offset(context)?; + let length = ta.byte_length(context)?; + + if let Ok(array_buffer) = JsArrayBuffer::from_object(buffer_obj.clone()) { + if let Some(buffer_data) = array_buffer.data() { + let bytes = buffer_data.as_ref(); + request_body = bytes[offset..offset + length].to_vec(); + } else { + return Err( + js_error!(TypeError: "Request constructor: TypedArray buffer detached or unusable"), + ); + } + } else if let Ok(shared_buffer) = JsSharedArrayBuffer::from_object(buffer_obj) { + let bytes = shared_buffer.to_vec(); request_body = bytes[offset..offset + length].to_vec(); } else { return Err( - js_error!(TypeError: "Request constructor: TypedArray buffer detached or unusable"), + js_error!(TypeError: "Request constructor: TypedArray buffer is not an ArrayBuffer or SharedArrayBuffer"), ); } } else if let Ok(dv) = JsDataView::from_object(object.clone()) { let buffer = dv.buffer(context)?; - let array_buffer = JsArrayBuffer::from_object( - buffer - .as_object() - .expect("buffer must be an object") - .clone(), - )?; - if let Some(bytes) = array_buffer.to_vec() { - let offset = dv.byte_offset(context)? as usize; - let length = dv.byte_length(context)? as usize; + let buffer_obj = buffer + .as_object() + .ok_or_else(|| { + js_error!(TypeError: "Request constructor: DataView buffer must be an object") + })? + .clone(); + + let offset = usize::try_from(dv.byte_offset(context)?).unwrap_or(0); + let length = usize::try_from(dv.byte_length(context)?).unwrap_or(0); + + if let Ok(array_buffer) = JsArrayBuffer::from_object(buffer_obj.clone()) { + if let Some(buffer_data) = array_buffer.data() { + let bytes = buffer_data.as_ref(); + request_body = bytes[offset..offset + length].to_vec(); + } else { + return Err( + js_error!(TypeError: "Request constructor: DataView buffer detached or unusable"), + ); + } + } else if let Ok(shared_buffer) = JsSharedArrayBuffer::from_object(buffer_obj) { + let bytes = shared_buffer.to_vec(); request_body = bytes[offset..offset + length].to_vec(); } else { return Err( - js_error!(TypeError: "Request constructor: DataView buffer detached or unusable"), + js_error!(TypeError: "Request constructor: DataView buffer is not an ArrayBuffer or SharedArrayBuffer"), ); } } else { diff --git a/core/runtime/src/fetch/tests/request.rs b/core/runtime/src/fetch/tests/request.rs index 7429bd4d87a..35fcd89a842 100644 --- a/core/runtime/src/fetch/tests/request.rs +++ b/core/runtime/src/fetch/tests/request.rs @@ -174,6 +174,24 @@ fn request_body_typedarray() { method: "POST", body: dv, }); + // Uint8Array subarray exercising offset/length slicing ("ell") + const sub = buf.subarray(1, 4); + globalThis.req3 = new Request("http://unit.test", { + method: "POST", + body: sub, + }); + // DataView with non-zero byteOffset and explicit byteLength ("ell") + const dvSlice = new DataView(buf.buffer, 1, 3); + globalThis.req4 = new Request("http://unit.test", { + method: "POST", + body: dvSlice, + }); + // Plain ArrayBuffer body ("hello") + const ab = buf.buffer; + globalThis.req5 = new Request("http://unit.test", { + method: "POST", + body: ab, + }); "#, ), TestAction::inspect_context(|ctx| { @@ -186,6 +204,21 @@ fn request_body_typedarray() { let request2_obj = request2.as_object().unwrap(); let request2 = request2_obj.downcast_ref::().unwrap(); assert_eq!(request2.inner().body().as_slice(), b"hello"); + + let request3 = ctx.global_object().get(js_str!("req3"), ctx).unwrap(); + let request3_obj = request3.as_object().unwrap(); + let request3 = request3_obj.downcast_ref::().unwrap(); + assert_eq!(request3.inner().body().as_slice(), b"ell"); + + let request4 = ctx.global_object().get(js_str!("req4"), ctx).unwrap(); + let request4_obj = request4.as_object().unwrap(); + let request4 = request4_obj.downcast_ref::().unwrap(); + assert_eq!(request4.inner().body().as_slice(), b"ell"); + + let request5 = ctx.global_object().get(js_str!("req5"), ctx).unwrap(); + let request5_obj = request5.as_object().unwrap(); + let request5 = request5_obj.downcast_ref::().unwrap(); + assert_eq!(request5.inner().body().as_slice(), b"hello"); }), ]); }