Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions core/runtime/src/fetch/body.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<u8>>, 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<Vec<u8>>, 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<Vec<u8>>, 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::<serde_json::Value>(&json_string)
.map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?;

JsValue::from_json(&json, &mut context.borrow_mut())
},
context,
)
}
14 changes: 14 additions & 0 deletions core/runtime/src/fetch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -124,6 +125,9 @@ async fn fetch_inner<T: Fetcher>(
// 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<Vec<u8>> = match resource {
Either::Left(url) => {
Expand All @@ -144,7 +148,13 @@ async fn fetch_inner<T: Fetcher>(
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()
}
};
Expand All @@ -167,6 +177,10 @@ async fn fetch_inner<T: Fetcher>(
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?;
Expand Down
155 changes: 131 additions & 24 deletions core/runtime/src/fetch/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsValue>,
Expand All @@ -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.
///
Expand Down Expand Up @@ -115,19 +122,32 @@ pub struct JsRequest {
#[unsafe_ignore_trace]
inner: HttpRequest<Vec<u8>>,
signal: Option<JsObject>,
#[unsafe_ignore_trace]
has_body: bool,
#[unsafe_ignore_trace]
body_used: Cell<bool>,
}

impl JsRequest {
fn new(inner: HttpRequest<Vec<u8>>, signal: Option<JsObject>, 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<Vec<u8>> {
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<Vec<u8>>, Option<JsObject>) {
/// Split this request into its HTTP request, abort signal, and body state.
fn into_parts(mut self) -> (HttpRequest<Vec<u8>>, Option<JsObject>, 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.
Expand All @@ -140,6 +160,44 @@ impl JsRequest {
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);
}
}

// 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 <https://fetch.spec.whatwg.org/#concept-body-consume-body>.
fn consume_body(&self) -> JsResult<Rc<Vec<u8>>> {
Comment thread
Monti-27 marked this conversation as resolved.
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()
Expand All @@ -154,7 +212,9 @@ impl JsRequest {
input: Either<JsString, JsRequest>,
options: Option<RequestInit>,
) -> JsResult<Self> {
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()
Expand All @@ -165,30 +225,30 @@ impl JsRequest {
.uri(uri)
.body(Vec::<u8>::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<HttpRequest<Vec<u8>>> for JsRequest {
fn from(inner: HttpRequest<Vec<u8>>) -> Self {
Self {
inner,
signal: None,
}
let has_body = !inner.body().is_empty();
Self::new(inner, None, has_body)
}
}

Expand All @@ -203,23 +263,70 @@ impl JsRequest {
input: Either<JsString, JsObject>,
options: Option<RequestInit>,
) -> JsResult<Self> {
// 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::<JsRequest>() {
Either::Right(request.borrow().data().clone())
if let Ok(request_obj) = r.clone().downcast::<JsRequest>() {
{
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)
}

/// Returns whether the request body has been consumed.
///
/// See <https://fetch.spec.whatwg.org/#dom-body-bodyused>.
#[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()
Comment thread
Monti-27 marked this conversation as resolved.
}

/// Returns a copy of this request.
///
/// See <https://fetch.spec.whatwg.org/#dom-request-clone>.
#[boa(rename = "clone")]
fn clone_request(&self) -> Self {
self.clone()
fn clone_request(&self) -> JsResult<Self> {
// 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())
}
Comment thread
Monti-27 marked this conversation as resolved.

fn bytes(&self, context: &mut Context) -> JsResult<JsPromise> {
Ok(body::bytes(self.consume_body()?, context))
}

fn text(&self, context: &mut Context) -> JsResult<JsPromise> {
Ok(body::text(self.consume_body()?, context))
}

fn json(&self, context: &mut Context) -> JsResult<JsPromise> {
Ok(body::json(self.consume_body()?, context))
}
}
34 changes: 5 additions & 29 deletions core/runtime/src/fetch/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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::<serde_json::Value>(&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)
}
}
Loading
Loading