From 8bfe6f049277ab9940be338d163799b9af87c312 Mon Sep 17 00:00:00 2001 From: Monti-27 Date: Sun, 12 Apr 2026 02:10:48 +0530 Subject: [PATCH 1/4] share fetch body readers --- core/runtime/src/fetch/body.rs | 35 ++++++++++++++++++++++++++++++ core/runtime/src/fetch/mod.rs | 1 + core/runtime/src/fetch/response.rs | 34 +++++------------------------ 3 files changed, 41 insertions(+), 29 deletions(-) create mode 100644 core/runtime/src/fetch/body.rs diff --git a/core/runtime/src/fetch/body.rs b/core/runtime/src/fetch/body.rs new file mode 100644 index 00000000000..e36fba42b35 --- /dev/null +++ b/core/runtime/src/fetch/body.rs @@ -0,0 +1,35 @@ +use boa_engine::object::builtins::{JsPromise, JsUint8Array}; +use boa_engine::{Context, JsNativeError, JsString, JsValue}; +use std::rc::Rc; + +pub(super) fn bytes(body: Rc>, context: &mut Context) -> JsPromise { + JsPromise::from_async_fn( + async move |context| { + JsUint8Array::from_iter(body.iter().copied(), &mut context.borrow_mut()).map(Into::into) + }, + context, + ) +} + +pub(super) fn text(body: Rc>, context: &mut Context) -> JsPromise { + JsPromise::from_async_fn( + async move |_| { + let body = String::from_utf8_lossy(body.as_ref()); + Ok(JsString::from(body).into()) + }, + context, + ) +} + +pub(super) fn json(body: Rc>, context: &mut Context) -> JsPromise { + JsPromise::from_async_fn( + async move |context| { + let json_string = String::from_utf8_lossy(body.as_ref()); + let json = serde_json::from_str::(&json_string) + .map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?; + + JsValue::from_json(&json, &mut context.borrow_mut()) + }, + context, + ) +} diff --git a/core/runtime/src/fetch/mod.rs b/core/runtime/src/fetch/mod.rs index f9b394fa6a2..d1c6f59df9f 100644 --- a/core/runtime/src/fetch/mod.rs +++ b/core/runtime/src/fetch/mod.rs @@ -24,6 +24,7 @@ use http::{HeaderName, HeaderValue, Request as HttpRequest, Request}; use std::cell::RefCell; use std::rc::Rc; +mod body; pub mod headers; pub mod headers_iterator; pub mod request; diff --git a/core/runtime/src/fetch/response.rs b/core/runtime/src/fetch/response.rs index cd13f55eba5..a023935f1f0 100644 --- a/core/runtime/src/fetch/response.rs +++ b/core/runtime/src/fetch/response.rs @@ -4,9 +4,9 @@ //! See the [Response interface documentation][mdn] for more information. //! //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Response - +use crate::fetch::body; use crate::fetch::headers::JsHeaders; -use boa_engine::object::builtins::{JsPromise, JsUint8Array}; +use boa_engine::object::builtins::JsPromise; use boa_engine::value::{Convert, TryFromJs, TryIntoJs}; use boa_engine::{ Context, JsData, JsNativeError, JsResult, JsString, JsValue, boa_class, js_error, js_str, @@ -459,38 +459,14 @@ impl JsResponse { } fn bytes(&self, context: &mut Context) -> JsPromise { - let body = self.body.clone(); - JsPromise::from_async_fn( - async move |context| { - JsUint8Array::from_iter(body.iter().copied(), &mut context.borrow_mut()) - .map(Into::into) - }, - context, - ) + body::bytes(self.body.clone(), context) } fn text(&self, context: &mut Context) -> JsPromise { - let body = self.body.clone(); - JsPromise::from_async_fn( - async move |_| { - let body = String::from_utf8_lossy(body.as_ref()); - Ok(JsString::from(body).into()) - }, - context, - ) + body::text(self.body.clone(), context) } fn json(&self, context: &mut Context) -> JsPromise { - let body = self.body.clone(); - JsPromise::from_async_fn( - async move |context| { - let json_string = String::from_utf8_lossy(body.as_ref()); - let json = serde_json::from_str::(&json_string) - .map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?; - - JsValue::from_json(&json, &mut context.borrow_mut()) - }, - context, - ) + body::json(self.body.clone(), context) } } From 527bf9f14d79f6c3dac4a27b18b003dddde95f66 Mon Sep 17 00:00:00 2001 From: Monti-27 Date: Sun, 12 Apr 2026 02:12:02 +0530 Subject: [PATCH 2/4] add request body support --- core/runtime/src/fetch/mod.rs | 13 +++ core/runtime/src/fetch/request.rs | 151 +++++++++++++++++++++++------- 2 files changed, 131 insertions(+), 33 deletions(-) diff --git a/core/runtime/src/fetch/mod.rs b/core/runtime/src/fetch/mod.rs index d1c6f59df9f..eaea44d6bbe 100644 --- a/core/runtime/src/fetch/mod.rs +++ b/core/runtime/src/fetch/mod.rs @@ -125,6 +125,9 @@ async fn fetch_inner( // The resource parsing is complicated, so we parse it in Rust here (instead of relying on // `TryFromJs` and friends). let mut signal = signal; + let mut source_request = None; + let mut reuse_source_body = false; + let body_overridden = options.as_ref().is_some_and(RequestInit::has_body); let request: Request> = match resource { Either::Left(url) => { @@ -145,7 +148,13 @@ async fn fetch_inner( return Err(js_error!(TypeError: "Request object is already in use")); }; + if !body_overridden { + request_ref.data().ensure_body_unused()?; + } + signal = signal.or_else(|| request_ref.data().signal()); + reuse_source_body = !body_overridden && request_ref.data().has_body(); + source_request = Some(request.clone()); request_ref.data().inner().clone() } }; @@ -168,6 +177,10 @@ async fn fetch_inner( request.headers_mut().append("Accept-Language", lang); } + if reuse_source_body && let Some(source_request) = source_request { + source_request.borrow().data().mark_body_used(); + } + let response = fetcher .fetch(JsRequest::from(request), signal.clone(), context) .await?; diff --git a/core/runtime/src/fetch/request.rs b/core/runtime/src/fetch/request.rs index 2991dfdd9a2..7d987a765b3 100644 --- a/core/runtime/src/fetch/request.rs +++ b/core/runtime/src/fetch/request.rs @@ -4,19 +4,22 @@ //! //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request use super::HttpRequest; +use super::body; use super::headers::JsHeaders; +use boa_engine::object::builtins::JsPromise; use boa_engine::value::{Convert, TryFromJs}; use boa_engine::{ - Finalize, JsData, JsObject, JsResult, JsString, JsValue, Trace, boa_class, js_error, + Context, Finalize, JsData, JsObject, JsResult, JsString, JsValue, Trace, boa_class, js_error, }; use either::Either; +use std::cell::Cell; use std::mem; +use std::rc::Rc; /// A [RequestInit][mdn] object. This is a JavaScript object (not a /// class) that can be used as options for creating a [`JsRequest`]. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/RequestInit -// TODO: This class does not contain all fields that are defined in the spec. #[derive(Debug, Clone, TryFromJs, Trace, Finalize)] pub struct RequestInit { body: Option, @@ -31,6 +34,10 @@ impl RequestInit { self.signal.take() } + pub(crate) fn has_body(&self) -> bool { + self.body.is_some() + } + /// Create an [`http::request::Builder`] object and return both the /// body specified by JavaScript and the builder. /// @@ -66,12 +73,6 @@ impl RequestInit { |_| js_error!(TypeError: "Request constructor: {} is an invalid method", method.to_std_string_escaped()), )?; - // 25. If init["method"] exists, then: - // 1. Let method be init["method"]. - // 2. If method is not a method or method is a forbidden method, throw a TypeError. - // 3. Normalize method. - // 4. Set request's method to method. - // https://fetch.spec.whatwg.org/#dom-request if method.eq_ignore_ascii_case("CONNECT") || method.eq_ignore_ascii_case("TRACE") || method.eq_ignore_ascii_case("TRACK") @@ -86,7 +87,6 @@ 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( |_| js_error!(TypeError: "Request constructor: body is not a valid string"), @@ -110,24 +110,47 @@ impl RequestInit { /// The `Request` interface of the [Fetch API][mdn] represents a resource request. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API -#[derive(Clone, Debug, JsData, Trace, Finalize)] +#[derive(Debug, JsData, Trace, Finalize)] pub struct JsRequest { #[unsafe_ignore_trace] inner: HttpRequest>, signal: Option, + #[unsafe_ignore_trace] + has_body: bool, + #[unsafe_ignore_trace] + body_used: Cell, +} + +impl Clone for JsRequest { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + signal: self.signal.clone(), + has_body: self.has_body, + body_used: Cell::new(self.body_used.get()), + } + } } impl JsRequest { + fn new(inner: HttpRequest>, signal: Option, has_body: bool) -> Self { + Self { + inner, + signal, + has_body, + body_used: Cell::new(false), + } + } + /// Get the inner `http::Request` object. This drops the body (if any). pub fn into_inner(mut self) -> HttpRequest> { mem::replace(&mut self.inner, HttpRequest::new(Vec::new())) } - /// Split this request into its HTTP request and abort signal. - fn into_parts(mut self) -> (HttpRequest>, Option) { + fn into_parts(mut self) -> (HttpRequest>, Option, bool) { let request = mem::replace(&mut self.inner, HttpRequest::new(Vec::new())); let signal = self.signal.take(); - (request, signal) + (request, signal, self.has_body) } /// Get a reference to the inner `http::Request` object. @@ -135,11 +158,37 @@ impl JsRequest { &self.inner } - /// Get the abort signal associated with this request, if any. pub(crate) fn signal(&self) -> Option { self.signal.clone() } + pub(crate) fn has_body(&self) -> bool { + self.has_body + } + + pub(crate) fn ensure_body_unused(&self) -> JsResult<()> { + if self.is_body_used() { + return Err(js_error!(TypeError: "Body has already been used")); + } + Ok(()) + } + + pub(crate) fn mark_body_used(&self) { + if self.has_body { + self.body_used.set(true); + } + } + + fn consume_body(&self) -> JsResult>> { + self.ensure_body_unused()?; + self.mark_body_used(); + Ok(Rc::new(self.inner.body().clone())) + } + + fn is_body_used(&self) -> bool { + self.has_body && self.body_used.get() + } + /// Get the URI of the request. pub fn uri(&self) -> &http::Uri { self.inner.uri() @@ -154,7 +203,9 @@ impl JsRequest { input: Either, options: Option, ) -> JsResult { - let (request, signal) = match input { + let body_overridden = options.as_ref().is_some_and(RequestInit::has_body); + + let (request, signal, has_body) = match input { Either::Left(uri) => { let uri = http::Uri::try_from( uri.to_std_string() @@ -165,30 +216,30 @@ impl JsRequest { .uri(uri) .body(Vec::::new()) .map_err(|_| js_error!(Error: "Cannot construct request"))?; - (request, None) + (request, None, false) + } + Either::Right(r) => { + if !body_overridden { + r.ensure_body_unused()?; + } + r.into_parts() } - Either::Right(r) => r.into_parts(), }; if let Some(mut options) = options { let signal = options.take_signal().or(signal); let inner = options.into_request_builder(Some(request))?; - Ok(Self { inner, signal }) + Ok(Self::new(inner, signal, body_overridden || has_body)) } else { - Ok(Self { - inner: request, - signal, - }) + Ok(Self::new(request, signal, has_body)) } } } impl From>> for JsRequest { fn from(inner: HttpRequest>) -> Self { - Self { - inner, - signal: None, - } + let has_body = !inner.body().is_empty(); + Self::new(inner, None, has_body) } } @@ -203,23 +254,57 @@ impl JsRequest { input: Either, options: Option, ) -> JsResult { - // Need to use a match as `Either::map_right` does not have an equivalent - // `Either::map_right_ok`. + let body_overridden = options.as_ref().is_some_and(RequestInit::has_body); + let mut source_request = None; let input = match input { Either::Right(r) => { - if let Ok(request) = r.clone().downcast::() { - Either::Right(request.borrow().data().clone()) + if let Ok(request_obj) = r.clone().downcast::() { + { + let request_ref = request_obj.borrow(); + let request = request_ref.data(); + if !body_overridden { + request.ensure_body_unused()?; + } + source_request = Some(request_obj.clone()); + } + + let request = request_obj.borrow(); + Either::Right(request.data().clone()) } else { return Err(js_error!(TypeError: "invalid input argument")); } } Either::Left(i) => Either::Left(i), }; - JsRequest::create_from_js(input, options) + let request = JsRequest::create_from_js(input, options)?; + + if !body_overridden && let Some(source_request) = source_request { + source_request.borrow().data().mark_body_used(); + } + + Ok(request) + } + + #[boa(getter)] + fn body_used(&self) -> bool { + self.is_body_used() } #[boa(rename = "clone")] - fn clone_request(&self) -> Self { - self.clone() + fn clone_request(&self) -> JsResult { + self.ensure_body_unused()?; + Ok(self.clone()) + } + + fn bytes(&self, context: &mut Context) -> JsResult { + Ok(body::bytes(self.consume_body()?, context)) + } + + fn text(&self, context: &mut Context) -> JsResult { + Ok(body::text(self.consume_body()?, context)) + } + + fn json(&self, context: &mut Context) -> JsResult { + Ok(body::json(self.consume_body()?, context)) } } From 768ff6ed619fe188ee13251e37e7bf3f8d75aa6c Mon Sep 17 00:00:00 2001 From: Monti-27 Date: Sun, 12 Apr 2026 02:12:13 +0530 Subject: [PATCH 3/4] add request body tests --- core/runtime/src/fetch/tests/request.rs | 248 ++++++++++++++++++++++++ 1 file changed, 248 insertions(+) diff --git a/core/runtime/src/fetch/tests/request.rs b/core/runtime/src/fetch/tests/request.rs index b14a7c84859..e5c344b83a3 100644 --- a/core/runtime/src/fetch/tests/request.rs +++ b/core/runtime/src/fetch/tests/request.rs @@ -354,3 +354,251 @@ fn request_clone_signal_override() { }), ]); } + +#[test] +fn request_body_methods() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| { + let fetcher = TestFetcher::default(); + crate::fetch::register(fetcher, None, ctx).expect("failed to register fetch"); + }), + TestAction::run( + r#" + globalThis.promise = (async () => { + const textRequest = new Request("http://unit.test", { + method: "POST", + body: "", + }); + assertEq(textRequest.bodyUsed, false); + assertEq(await textRequest.text(), ""); + assertEq(textRequest.bodyUsed, true); + + const bytesRequest = new Request("http://unit.test", { + method: "POST", + body: "hello", + }); + const bytes = await bytesRequest.bytes(); + assertEq(new TextDecoder().decode(bytes), "hello"); + assertEq(bytesRequest.bodyUsed, true); + + const jsonRequest = new Request("http://unit.test", { + method: "POST", + body: '{ "value": 1 }', + }); + const json = await jsonRequest.json(); + assertEq(json.value, 1); + assertEq(jsonRequest.bodyUsed, true); + })(); + "#, + ), + TestAction::inspect_context(|ctx| { + let promise = ctx.global_object().get(js_str!("promise"), ctx).unwrap(); + promise.as_promise().unwrap().await_blocking(ctx).unwrap(); + }), + ]); +} + +#[test] +fn request_without_body_is_not_disturbed_by_reads() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| { + let fetcher = TestFetcher::default(); + crate::fetch::register(fetcher, None, ctx).expect("failed to register fetch"); + }), + TestAction::run( + r#" + globalThis.promise = (async () => { + const request = new Request("http://unit.test"); + assertEq(await request.text(), ""); + assertEq(await request.text(), ""); + assertEq(request.bodyUsed, false); + const cloned = request.clone(); + assertEq(cloned instanceof Request, true); + })(); + "#, + ), + TestAction::inspect_context(|ctx| { + let promise = ctx.global_object().get(js_str!("promise"), ctx).unwrap(); + promise.as_promise().unwrap().await_blocking(ctx).unwrap(); + }), + ]); +} + +#[test] +fn request_used_body_cannot_be_reused() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| { + let fetcher = TestFetcher::default(); + crate::fetch::register(fetcher, None, ctx).expect("failed to register fetch"); + }), + TestAction::run( + r#" + globalThis.promise = (async () => { + const request = new Request("http://unit.test", { + method: "POST", + body: "payload", + }); + + assertEq(await request.text(), "payload"); + + for (const action of [ + () => request.clone(), + () => new Request(request), + ]) { + try { + action(); + throw Error("expected the call above to throw"); + } catch (e) { + if (!(e instanceof TypeError)) { + throw e; + } + } + } + + const overridden = new Request(request, { + method: "POST", + body: "override", + }); + assertEq(await overridden.text(), "override"); + })(); + "#, + ), + TestAction::inspect_context(|ctx| { + let promise = ctx.global_object().get(js_str!("promise"), ctx).unwrap(); + promise.as_promise().unwrap().await_blocking(ctx).unwrap(); + }), + ]); +} + +#[test] +fn request_constructor_consumes_source_body() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| { + let fetcher = TestFetcher::default(); + crate::fetch::register(fetcher, None, ctx).expect("failed to register fetch"); + }), + TestAction::run( + r#" + globalThis.promise = (async () => { + const withBody = new Request("http://unit.test", { + method: "POST", + body: "payload", + }); + const copied = new Request(withBody); + assertEq(withBody.bodyUsed, true); + assertEq(await copied.text(), "payload"); + + const withEmptyBody = new Request("http://unit.test", { + method: "POST", + body: "", + }); + const copiedEmpty = new Request(withEmptyBody); + assertEq(withEmptyBody.bodyUsed, true); + assertEq(await copiedEmpty.text(), ""); + + const withoutBody = new Request("http://unit.test"); + const copiedWithoutBody = new Request(withoutBody); + assertEq(withoutBody.bodyUsed, false); + assertEq(await copiedWithoutBody.text(), ""); + + const overridden = new Request("http://unit.test", { + method: "POST", + body: "payload", + }); + const overrideCopy = new Request(overridden, { + method: "POST", + body: "override", + }); + assertEq(overridden.bodyUsed, false); + assertEq(await overrideCopy.text(), "override"); + })(); + "#, + ), + TestAction::inspect_context(|ctx| { + let promise = ctx.global_object().get(js_str!("promise"), ctx).unwrap(); + promise.as_promise().unwrap().await_blocking(ctx).unwrap(); + }), + ]); +} + +#[test] +fn request_constructor_does_not_consume_source_when_it_throws() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| { + let fetcher = TestFetcher::default(); + crate::fetch::register(fetcher, None, ctx).expect("failed to register fetch"); + }), + TestAction::run( + r#" + const request = new Request("http://unit.test", { + method: "POST", + body: "payload", + }); + + try { + new Request(request, { method: "CONNECT" }); + throw Error("expected the call above to throw"); + } catch (e) { + if (!(e instanceof TypeError)) { + throw e; + } + } + + assertEq(request.bodyUsed, false); + "#, + ), + ]); +} + +#[test] +fn fetch_marks_request_body_used() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| { + let mut fetcher = TestFetcher::default(); + fetcher.add_response( + Uri::from_static("http://unit.test"), + Response::new("ok".as_bytes().to_vec()), + ); + crate::fetch::register(fetcher, None, ctx).expect("failed to register fetch"); + }), + TestAction::run( + r#" + globalThis.promise = (async () => { + const request = new Request("http://unit.test", { + method: "POST", + body: "payload", + }); + + const response = await fetch(request); + assertEq(await response.text(), "ok"); + assertEq(request.bodyUsed, true); + + try { + await fetch(request); + throw Error("expected the call above to throw"); + } catch (e) { + if (!(e instanceof TypeError)) { + throw e; + } + } + + const overrideResponse = await fetch(request, { + method: "POST", + body: "override", + }); + assertEq(await overrideResponse.text(), "ok"); + })(); + "#, + ), + TestAction::inspect_context(|ctx| { + let promise = ctx.global_object().get(js_str!("promise"), ctx).unwrap(); + promise.as_promise().unwrap().await_blocking(ctx).unwrap(); + }), + ]); +} From 0b5b0044d963624f9a77853ed95d1397441633a6 Mon Sep 17 00:00:00 2001 From: Monti-27 Date: Wed, 15 Apr 2026 00:48:00 +0530 Subject: [PATCH 4/4] update request docs --- core/runtime/src/fetch/request.rs | 46 +++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/core/runtime/src/fetch/request.rs b/core/runtime/src/fetch/request.rs index 7d987a765b3..d38f1b56aab 100644 --- a/core/runtime/src/fetch/request.rs +++ b/core/runtime/src/fetch/request.rs @@ -73,6 +73,12 @@ impl RequestInit { |_| js_error!(TypeError: "Request constructor: {} is an invalid method", method.to_std_string_escaped()), )?; + // 25. If init["method"] exists, then: + // 1. Let method be init["method"]. + // 2. If method is not a method or method is a forbidden method, throw a TypeError. + // 3. Normalize method. + // 4. Set request's method to method. + // https://fetch.spec.whatwg.org/#dom-request if method.eq_ignore_ascii_case("CONNECT") || method.eq_ignore_ascii_case("TRACE") || method.eq_ignore_ascii_case("TRACK") @@ -87,6 +93,7 @@ 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( |_| js_error!(TypeError: "Request constructor: body is not a valid string"), @@ -110,7 +117,7 @@ impl RequestInit { /// The `Request` interface of the [Fetch API][mdn] represents a resource request. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API -#[derive(Debug, JsData, Trace, Finalize)] +#[derive(Clone, Debug, JsData, Trace, Finalize)] pub struct JsRequest { #[unsafe_ignore_trace] inner: HttpRequest>, @@ -121,17 +128,6 @@ pub struct JsRequest { body_used: Cell, } -impl Clone for JsRequest { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - signal: self.signal.clone(), - has_body: self.has_body, - body_used: Cell::new(self.body_used.get()), - } - } -} - impl JsRequest { fn new(inner: HttpRequest>, signal: Option, has_body: bool) -> Self { Self { @@ -147,6 +143,7 @@ impl JsRequest { mem::replace(&mut self.inner, HttpRequest::new(Vec::new())) } + /// Split this request into its HTTP request, abort signal, and body state. fn into_parts(mut self) -> (HttpRequest>, Option, bool) { let request = mem::replace(&mut self.inner, HttpRequest::new(Vec::new())); let signal = self.signal.take(); @@ -158,6 +155,7 @@ impl JsRequest { &self.inner } + /// Get the abort signal associated with this request, if any. pub(crate) fn signal(&self) -> Option { self.signal.clone() } @@ -179,6 +177,17 @@ impl JsRequest { } } + // The consume body algorithm, given an object that includes Body and an + // algorithm that converts bytes to a JavaScript value, runs these steps: + // 1. If object is unusable, then return a promise rejected with a TypeError. + // TODO: 2-3. Create a promise and wire its success and error steps. + // 4. If object's body is null, then run successSteps with an empty byte sequence. + // TODO: 5. Fully read object's body stream. + // + // Boa currently models request bodies as eagerly buffered bytes, so after + // checking whether the body is unusable, we can return the stored bytes + // directly to the callers that build the resulting promise. + // See . fn consume_body(&self) -> JsResult>> { self.ensure_body_unused()?; self.mark_body_used(); @@ -285,13 +294,26 @@ impl JsRequest { Ok(request) } + /// Returns whether the request body has been consumed. + /// + /// See . #[boa(getter)] fn body_used(&self) -> bool { + // The bodyUsed getter steps are to return true if this's body is + // non-null and this's body's stream is disturbed; otherwise false. self.is_body_used() } + /// Returns a copy of this request. + /// + /// See . #[boa(rename = "clone")] fn clone_request(&self) -> JsResult { + // The clone() method steps are: + // 1. If this is unusable, then throw a TypeError. + // 2. Let clonedRequest be the result of cloning this's request. + // TODO: 4-6. Clone the associated signal by creating a dependent abort signal. + // 7. Return the cloned request object. self.ensure_body_unused()?; Ok(self.clone()) }