diff --git a/core/runtime/src/fetch/response.rs b/core/runtime/src/fetch/response.rs index 442ed64d5eb..77a6579619d 100644 --- a/core/runtime/src/fetch/response.rs +++ b/core/runtime/src/fetch/response.rs @@ -6,7 +6,9 @@ //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Response use crate::fetch::headers::JsHeaders; -use boa_engine::object::builtins::{JsPromise, JsUint8Array}; +use boa_engine::object::builtins::{ + JsArrayBuffer, JsDataView, JsPromise, JsTypedArray, JsUint8Array, +}; use boa_engine::value::{Convert, TryFromJs, TryIntoJs}; use boa_engine::{ Context, JsData, JsNativeError, JsResult, JsString, JsValue, boa_class, js_error, js_str, @@ -277,9 +279,47 @@ fn initialize_response( /// /// See fn extract_body(val: &JsValue, context: &mut Context) -> JsResult<(Vec, Option<&'static str>)> { - // TODO: handle other BodyInit types: Blob, ArrayBuffer, ArrayBufferView, - // FormData, URLSearchParams. - // Currently only USVString is supported. + if let Some(obj) = val.as_object() { + if let Ok(ab) = JsArrayBuffer::from_object(obj.clone()) { + let bytes = ab + .to_vec() + .ok_or_else(|| JsNativeError::typ().with_message("ArrayBuffer is detached"))?; + return Ok((bytes, None)); + } + + if let Ok(ta) = JsTypedArray::from_object(obj.clone()) { + let offset = ta.byte_offset(context)?; + let len = ta.byte_length(context)?; + let buffer_obj = ta.buffer(context)?.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("TypedArray buffer is not an object") + })?; + + let ab = JsArrayBuffer::from_object(buffer_obj)?; + let data = ab + .data() + .ok_or_else(|| JsNativeError::typ().with_message("ArrayBuffer is detached"))?; + return Ok((data[offset..offset + len].to_vec(), None)); + } + + if let Ok(dv) = JsDataView::from_object(obj.clone()) { + let offset = usize::try_from(dv.byte_offset(context)?) + .map_err(|e| JsNativeError::range().with_message(e.to_string()))?; + let len = usize::try_from(dv.byte_length(context)?) + .map_err(|e| JsNativeError::range().with_message(e.to_string()))?; + let buffer_obj = dv.buffer(context)?.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("DataView buffer is not an object") + })?; + + let ab = JsArrayBuffer::from_object(buffer_obj)?; + let data = ab + .data() + .ok_or_else(|| JsNativeError::typ().with_message("ArrayBuffer is detached"))?; + return Ok((data[offset..offset + len].to_vec(), None)); + } + } + + // TODO: handle other BodyInit types: Blob, FormData, URLSearchParams. + // Currently Strings and primitives fallback here. // // For a USVString, the type is "text/plain;charset=UTF-8". // See https://fetch.spec.whatwg.org/#concept-bodyinit-extract step 6. diff --git a/core/runtime/src/fetch/tests/response.rs b/core/runtime/src/fetch/tests/response.rs index ee8108751d8..31f03423aa9 100644 --- a/core/runtime/src/fetch/tests/response.rs +++ b/core/runtime/src/fetch/tests/response.rs @@ -1,6 +1,6 @@ use super::TestFetcher; use crate::test::{TestAction, run_test_actions}; -use boa_engine::{Context, js_str}; +use boa_engine::{Context, JsValue, js_str}; use http::{Response, Uri}; fn register(responses: &[(&'static str, Response>)], ctx: &mut Context) { @@ -354,3 +354,56 @@ fn response_clone_preserves_status() { }), ]); } + +#[test] +fn response_body_buffer_sources() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| register(&[], ctx)), + TestAction::run( + r#" + // ArrayBuffer body + let ab = new Uint8Array([104,101,108,108,111]).buffer; + let r0 = new Response(ab); + assertEq(r0.headers.has("content-type"), false); + + // Uint8Array body (no content-type should be injected) + let u8a = new Uint8Array([104,101,108,108,111]); + let r1 = new Response(u8a); + assertEq(r1.headers.has("content-type"), false); + + // Subarray/view slicing + let sub = new Uint8Array([0,104,101,108,108,111,0]).subarray(1, 6); + let r2 = new Response(sub); + assertEq(r2.headers.has("content-type"), false); + + // DataView with non-zero offset + let buffer = new Uint8Array([0,104,101,108,108,111,0]).buffer; + let dv = new DataView(buffer, 1, 5); + let r3 = new Response(dv); + assertEq(r3.headers.has("content-type"), false); + + // String body fallback (should retain default text content-type) + let r4 = new Response("hello"); + assertEq(r4.headers.get("content-type"), "text/plain;charset=UTF-8"); + + globalThis.verify = async () => { + assertEq(await r0.text(), "hello"); + assertEq(await r1.text(), "hello"); + assertEq(await r2.text(), "hello"); + assertEq(await r3.text(), "hello"); + assertEq(await r4.text(), "hello"); + }; + "#, + ), + TestAction::inspect_context(|ctx| { + let verify_fn = ctx.global_object().get(js_str!("verify"), ctx).unwrap(); + let promise = verify_fn + .as_callable() + .unwrap() + .call(&JsValue::undefined(), &[], ctx) + .unwrap(); + promise.as_promise().unwrap().await_blocking(ctx).unwrap(); + }), + ]); +}