use super::HttpRequest;
use super::headers::JsHeaders;
use boa_engine::object::builtins::JsPromise;
use boa_engine::value::{Convert, TryFromJs};
use boa_engine::{
Context, Finalize, JsData, JsNativeError, JsObject, JsResult, JsString, JsValue, Trace,
boa_class, js_error,
};
use either::Either;
use std::cell::RefCell;
use std::future::Future;
use std::mem;
use std::pin::Pin;
use std::rc::Rc;
enum BodyState {
Ready(Vec<u8>),
Pending(Pin<Box<dyn Future<Output = Vec<u8>> + 'static>>),
}
#[derive(Debug, Clone, TryFromJs, Trace, Finalize)]
pub struct RequestInit {
body: Option<JsValue>,
headers: Option<JsHeaders>,
method: Option<Convert<JsString>>,
signal: Option<JsObject>,
}
impl RequestInit {
pub fn has_body(&self) -> bool {
self.body.is_some()
}
pub fn take_signal(&mut self) -> Option<JsObject> {
self.signal.take()
}
pub fn into_request_builder(
mut self,
request: Option<HttpRequest<Vec<u8>>>,
) -> JsResult<HttpRequest<Vec<u8>>> {
let mut builder = HttpRequest::builder();
let mut request_body = Vec::new();
if let Some(r) = request {
let (parts, body) = r.into_parts();
builder = builder
.method(parts.method)
.uri(parts.uri)
.version(parts.version);
for (key, value) in &parts.headers {
builder = builder.header(key, value);
}
request_body = body;
}
if let Some(headers) = self.headers.take() {
for (k, v) in headers.as_header_map().borrow().iter() {
builder = builder.header(k, v);
}
}
if let Some(Convert(ref method)) = self.method.take() {
let method = method.to_std_string().map_err(
|_| js_error!(TypeError: "Request constructor: {} is an invalid method", method.to_std_string_escaped()),
)?;
if method.eq_ignore_ascii_case("CONNECT")
|| method.eq_ignore_ascii_case("TRACE")
|| method.eq_ignore_ascii_case("TRACK")
{
return Err(js_error!(
TypeError: "'{}' HTTP method is unsupported.",
method
));
}
builder = builder.method(method.as_str());
}
if let Some(body) = &self.body {
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"),
)?;
request_body = body.into_bytes();
} else {
return Err(
js_error!(TypeError: "Request constructor: body is not a supported type"),
);
}
}
builder
.body(request_body)
.map_err(|_| js_error!(Error: "Cannot construct request"))
}
}
#[derive(Clone, JsData, Trace, Finalize)]
pub struct JsRequest {
#[unsafe_ignore_trace]
inner: HttpRequest<Vec<u8>>,
signal: Option<JsObject>,
#[unsafe_ignore_trace]
body: Rc<RefCell<BodyState>>,
}
impl std::fmt::Debug for JsRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JsRequest")
.field("method", &self.inner.method())
.field("uri", &self.inner.uri())
.finish_non_exhaustive()
}
}
impl JsRequest {
pub fn into_inner(self) -> HttpRequest<Vec<u8>> {
let body_bytes = match &*self.body.borrow() {
BodyState::Ready(b) => b.clone(),
BodyState::Pending(_) => Vec::new(),
};
let (parts, _) = self.inner.clone().into_parts();
HttpRequest::from_parts(parts, body_bytes)
}
fn into_parts(
mut self,
) -> (
HttpRequest<Vec<u8>>,
Option<JsObject>,
Rc<RefCell<BodyState>>,
) {
let request = mem::replace(&mut self.inner, HttpRequest::new(Vec::new()));
let signal = self.signal.take();
let body = Rc::clone(&self.body);
(request, signal, body)
}
pub fn inner(&self) -> &HttpRequest<Vec<u8>> {
&self.inner
}
pub(crate) fn signal(&self) -> Option<JsObject> {
self.signal.clone()
}
pub fn uri(&self) -> &http::Uri {
self.inner.uri()
}
pub fn create_from_js(
input: Either<JsString, JsRequest>,
options: Option<RequestInit>,
) -> JsResult<Self> {
let (request, signal, source_body) = match input {
Either::Left(uri) => {
let uri = http::Uri::try_from(
uri.to_std_string()
.map_err(|_| js_error!(URIError: "URI cannot have unpaired surrogates"))?,
)
.map_err(|_| js_error!(URIError: "Invalid URI"))?;
let request = http::request::Request::builder()
.uri(uri)
.body(Vec::<u8>::new())
.map_err(|_| js_error!(Error: "Cannot construct request"))?;
(request, None, None)
}
Either::Right(r) => {
let (request, signal, body) = r.into_parts();
(request, signal, Some(body))
}
};
if let Some(mut options) = options {
let signal = options.take_signal().or(signal);
let has_body = options.has_body();
let mut inner = options.into_request_builder(Some(request))?;
let body = if has_body {
let bytes = mem::take(inner.body_mut());
Rc::new(RefCell::new(BodyState::Ready(bytes)))
} else {
source_body.unwrap_or_else(|| Rc::new(RefCell::new(BodyState::Ready(Vec::new()))))
};
return Ok(Self {
inner,
signal,
body,
});
}
let body =
source_body.unwrap_or_else(|| Rc::new(RefCell::new(BodyState::Ready(Vec::new()))));
Ok(Self {
inner: request,
signal,
body,
})
}
pub fn with_lazy_body(
head: HttpRequest<Vec<u8>>,
body_future: impl Future<Output = Vec<u8>> + 'static,
) -> Self {
Self {
inner: head,
signal: None,
body: Rc::new(RefCell::new(BodyState::Pending(Box::pin(body_future)))),
}
}
}
impl From<HttpRequest<Vec<u8>>> for JsRequest {
fn from(mut inner: HttpRequest<Vec<u8>>) -> Self {
let bytes = mem::take(inner.body_mut());
Self {
inner,
signal: None,
body: Rc::new(RefCell::new(BodyState::Ready(bytes))),
}
}
}
async fn resolve_body(body_cell: Rc<RefCell<BodyState>>) -> Vec<u8> {
{
let guard = body_cell.borrow();
if let BodyState::Ready(ref bytes) = *guard {
return bytes.clone();
}
}
let fut = {
let mut guard = body_cell.borrow_mut();
match mem::replace(&mut *guard, BodyState::Ready(Vec::new())) {
BodyState::Pending(f) => f,
BodyState::Ready(_) => {
return Vec::new();
}
}
};
let bytes = fut.await;
*body_cell.borrow_mut() = BodyState::Ready(bytes.clone());
bytes
}
#[boa_class(rename = "Request")]
#[boa(rename_all = "camelCase")]
impl JsRequest {
#[boa(constructor)]
pub fn constructor(
input: Either<JsString, JsObject>,
options: Option<RequestInit>,
) -> JsResult<Self> {
let input = match input {
Either::Right(r) => {
if let Ok(request) = r.clone().downcast::<JsRequest>() {
Either::Right(request.borrow().data().clone())
} else {
return Err(js_error!(TypeError: "invalid input argument"));
}
}
Either::Left(i) => Either::Left(i),
};
JsRequest::create_from_js(input, options)
}
#[boa(rename = "clone")]
fn clone_request(&self) -> Self {
self.clone()
}
#[boa(getter)]
fn method(&self) -> JsString {
JsString::from(self.inner.method().as_str())
}
#[boa(getter)]
fn url(&self) -> JsString {
JsString::from(self.inner.uri().to_string().as_str())
}
#[boa(getter)]
fn headers(&self) -> JsHeaders {
JsHeaders::from_http(self.inner.headers().clone())
}
fn text(&self, context: &mut Context) -> JsPromise {
let body_cell = Rc::clone(&self.body);
JsPromise::from_async_fn(
async move |_| {
let bytes = resolve_body(body_cell).await;
let text = String::from_utf8_lossy(&bytes);
Ok(JsString::from(text.as_ref()).into())
},
context,
)
}
fn json(&self, context: &mut Context) -> JsPromise {
let body_cell = Rc::clone(&self.body);
JsPromise::from_async_fn(
async move |context| {
let bytes = resolve_body(body_cell).await;
let json_str = String::from_utf8_lossy(&bytes);
let json = serde_json::from_str::<serde_json::Value>(&json_str)
.map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?;
JsValue::from_json(&json, &mut context.borrow_mut())
},
context,
)
}
fn form_data(&self, context: &mut Context) -> JsPromise {
let body_cell = Rc::clone(&self.body);
let content_type = self
.inner
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.map(str::to_string);
JsPromise::from_async_fn(
async move |context| {
let is_url_encoded = content_type
.as_deref()
.map(|ct| ct.starts_with("application/x-www-form-urlencoded"))
.unwrap_or(true);
if !is_url_encoded {
return Err(JsNativeError::typ()
.with_message(
"formData() only supports application/x-www-form-urlencoded bodies",
)
.into());
}
let bytes = resolve_body(body_cell).await;
let ctx = &mut context.borrow_mut();
let form_obj = JsObject::default(ctx.intrinsics());
for (key, value) in form_urlencoded::parse(&bytes) {
form_obj.set(
JsString::from(key.as_ref()),
JsString::from(value.as_ref()),
false,
ctx,
)?;
}
Ok(form_obj.into())
},
context,
)
}
}