Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
151 changes: 118 additions & 33 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 @@ -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
Comment thread
Monti-27 marked this conversation as resolved.
if method.eq_ignore_ascii_case("CONNECT")
|| method.eq_ignore_ascii_case("TRACE")
|| method.eq_ignore_ascii_case("TRACK")
Expand All @@ -86,7 +87,6 @@ impl RequestInit {
}

if let Some(body) = &self.body {
// TODO: add more support types.
Comment thread
Monti-27 marked this conversation as resolved.
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"),
Expand All @@ -110,36 +110,85 @@ 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<Vec<u8>>,
signal: Option<JsObject>,
#[unsafe_ignore_trace]
has_body: bool,
#[unsafe_ignore_trace]
body_used: Cell<bool>,
}

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()),
}
}
}
Comment thread
Monti-27 marked this conversation as resolved.
Outdated

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>) {
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.
pub fn inner(&self) -> &HttpRequest<Vec<u8>> {
&self.inner
}

/// Get the abort signal associated with this request, if any.
Comment thread
Monti-27 marked this conversation as resolved.
pub(crate) fn signal(&self) -> Option<JsObject> {
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<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 +203,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 +216,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 +254,57 @@ 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)
}

#[boa(getter)]
fn body_used(&self) -> bool {
self.is_body_used()
Comment thread
Monti-27 marked this conversation as resolved.
}

#[boa(rename = "clone")]
fn clone_request(&self) -> Self {
self.clone()
fn clone_request(&self) -> JsResult<Self> {
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