pub mod abort;
pub mod backend;
pub mod body_init;
pub mod multipart;
pub mod streams;
pub use backend::{Client, FetchBackend, FetchFuture, FetchRequest};
pub use ferrijs_fetch as engine;
use std::sync::Arc;
use std::time::Duration;
use ferrijs_fetch::Headers as CoreHeaders;
use ferrijs_fetch::{Credentials, NetGuard, RedirectMode};
use ferrijs_std::abort::AbortSignal;
use ferrijs_std::stream_web::ReadableStream;
use rquickjs::atom::PredefinedAtom;
use rquickjs::function::{Opt, This};
use rquickjs::{Coerced, Ctx, IntoJs, Object, Value, class::Class, class::Trace};
use ferrijs_std::web::js_iterator::live_iterator;
use self::body_init::{BodySource, ExtractedBody, extract_body};
use crate::value::json_to_js;
use ferrijs_std::buffer::Blob;
use ferrijs_std::web::form_data::FormDataJs;
const MAX_FETCH_BODY_BYTES: usize = 64 * 1024 * 1024;
const FETCH_BODY_DRAIN_TIMEOUT: Duration = Duration::from_mins(2);
#[derive(Trace)]
#[rquickjs::class(rename = "Headers")]
pub struct HeadersJs {
#[qjs(skip_trace)]
list: CoreHeaders,
}
fn header_entry<'js>(
ctx: &Ctx<'js>,
parent: &Class<'js, HeadersJs>,
index: usize,
) -> rquickjs::Result<Option<Value<'js>>> {
let Some((name, value)) = parent.borrow().list.sorted_entries().into_iter().nth(index) else {
return Ok(None);
};
let pair = rquickjs::Array::new(ctx.clone())?;
pair.set(0, name)?;
pair.set(1, value)?;
Ok(Some(pair.into_value()))
}
fn header_key<'js>(
ctx: &Ctx<'js>,
parent: &Class<'js, HeadersJs>,
index: usize,
) -> rquickjs::Result<Option<Value<'js>>> {
parent
.borrow()
.list
.sorted_entries()
.into_iter()
.nth(index)
.map(|(name, _)| name.into_js(ctx))
.transpose()
}
fn header_val<'js>(
ctx: &Ctx<'js>,
parent: &Class<'js, HeadersJs>,
index: usize,
) -> rquickjs::Result<Option<Value<'js>>> {
parent
.borrow()
.list
.sorted_entries()
.into_iter()
.nth(index)
.map(|(_, value)| value.into_js(ctx))
.transpose()
}
#[derive(Trace)]
#[rquickjs::class(rename = "Response")]
pub struct FetchResponseJs<'js> {
#[qjs(skip_trace)]
status: u16,
#[qjs(skip_trace)]
status_text: String,
#[qjs(skip_trace)]
url: String,
#[qjs(skip_trace)]
headers: CoreHeaders,
#[qjs(skip_trace)]
body: Vec<u8>,
#[qjs(skip_trace)]
redirected: bool,
#[qjs(skip_trace)]
type_: &'static str,
#[qjs(skip_trace)]
body_used: bool,
#[qjs(skip_trace)]
net: Option<self::streams::NetBody>,
body_stream: Option<Class<'js, ReadableStream<'js>>>,
}
#[derive(Trace)]
#[rquickjs::class(rename = "Request")]
pub struct FetchRequestJs<'js> {
#[qjs(skip_trace)]
url: String,
#[qjs(skip_trace)]
method: String,
#[qjs(skip_trace)]
headers: CoreHeaders,
#[qjs(skip_trace)]
body: Vec<u8>,
#[qjs(skip_trace)]
redirect: String,
#[qjs(skip_trace)]
credentials: String,
#[qjs(skip_trace)]
body_used: bool,
#[qjs(skip_trace)]
cache: String,
#[qjs(skip_trace)]
mode: String,
#[qjs(skip_trace)]
referrer: String,
#[qjs(skip_trace)]
referrer_policy: String,
#[qjs(skip_trace)]
integrity: String,
#[qjs(skip_trace)]
keepalive: bool,
#[qjs(skip_trace)]
destination: String,
#[qjs(skip_trace)]
duplex: Option<String>,
#[qjs(skip_trace)]
signal_inner: Option<Arc<self::abort::AbortInner>>,
signal: Option<Class<'js, AbortSignal<'js>>>,
body_stream: Option<Class<'js, ReadableStream<'js>>>,
}
#[allow(unsafe_code)]
unsafe impl rquickjs::JsLifetime<'_> for HeadersJs {
type Changed<'to> = HeadersJs;
}
#[allow(unsafe_code)]
unsafe impl<'js> rquickjs::JsLifetime<'js> for FetchResponseJs<'js> {
type Changed<'to> = FetchResponseJs<'to>;
}
#[allow(unsafe_code)]
unsafe impl<'js> rquickjs::JsLifetime<'js> for FetchRequestJs<'js> {
type Changed<'to> = FetchRequestJs<'to>;
}
fn init_headers(init: Option<&Object<'_>>, body: Option<&ExtractedBody<'_>>) -> CoreHeaders {
let mut list = init
.and_then(|o| o.get::<_, Value<'_>>("headers").ok())
.map(|v| header_list_from(&v))
.unwrap_or_default();
apply_body_content_type(&mut list, body);
list
}
fn apply_body_content_type(list: &mut CoreHeaders, body: Option<&ExtractedBody<'_>>) {
let Some(body) = body else { return };
let Some(ct) = &body.content_type else { return };
if body.forced {
list.set("content-type", ct.clone());
} else {
list.set_if_absent("content-type", ct.clone());
}
}
fn header_list_from(v: &Value<'_>) -> CoreHeaders {
let mut list = CoreHeaders::new();
let _ = fill_header_list(None, &mut list, v);
list
}
fn fill_header_list(ctx: Option<&Ctx<'_>>, list: &mut CoreHeaders, v: &Value<'_>) -> rquickjs::Result<()> {
if let Ok(other) = Class::<HeadersJs>::from_value(v) {
for (name, value) in other.borrow().list.iter() {
list.append_combined(name.clone(), value.clone());
}
return Ok(());
}
let mut push = |raw_name: &str, raw_value: &str| -> rquickjs::Result<()> {
let valid_name = ferrijs_fetch::headers::is_valid_name(raw_name);
let value = ferrijs_fetch::headers::normalize_value(raw_value);
let valid_value = ferrijs_fetch::headers::is_valid_value(&value);
match ctx {
Some(ctx) if !valid_name => Err(rquickjs::Exception::throw_type(
ctx,
&format!("Invalid header name: {raw_name:?}"),
)),
Some(ctx) if !valid_value => Err(rquickjs::Exception::throw_type(ctx, "Invalid header value")),
_ => {
if valid_name && valid_value {
list.append_combined(raw_name.to_ascii_lowercase(), value);
}
Ok(())
},
}
};
if let Some(arr) = v.as_array() {
for i in 0..arr.len() {
let Ok(entry) = arr.get::<Value<'_>>(i) else { continue };
let pair = entry.as_array().filter(|p| p.len() == 2);
let Some(pair) = pair else {
match ctx {
Some(ctx) => {
return Err(rquickjs::Exception::throw_type(
ctx,
"Header init entry must be a [name, value] pair",
));
},
None => continue,
}
};
match (pair.get::<Coerced<String>>(0), pair.get::<Coerced<String>>(1)) {
(Ok(name), Ok(value)) => push(&name.0, &value.0)?,
_ if ctx.is_none() => {},
(name, value) => {
name?;
value?;
},
}
}
return Ok(());
}
if let Some(obj) = v.as_object() {
let keys = obj.keys::<String>().collect::<rquickjs::Result<Vec<_>>>();
let keys = match (keys, ctx) {
(Ok(keys), _) => keys,
(Err(e), Some(_)) => return Err(e),
(Err(_), None) => return Ok(()),
};
for name in keys {
match obj.get::<_, Coerced<String>>(name.as_str()) {
Ok(value) => push(&name, &value.0)?,
Err(_) if ctx.is_none() => {},
Err(e) => return Err(e),
}
}
}
Ok(())
}
impl HeadersJs {
pub(crate) fn from_pairs<I: IntoIterator<Item = (String, String)>>(it: I) -> Self {
let mut list = CoreHeaders::new();
for (name, value) in it {
list.append_combined(
name.to_ascii_lowercase(),
ferrijs_fetch::headers::normalize_value(&value),
);
}
Self { list }
}
fn check_name(ctx: &Ctx<'_>, name: &str) -> rquickjs::Result<String> {
if ferrijs_fetch::headers::is_valid_name(name) {
Ok(name.to_ascii_lowercase())
} else {
Err(rquickjs::Exception::throw_type(
ctx,
&format!("Invalid header name: {name:?}"),
))
}
}
fn check_value(ctx: &Ctx<'_>, raw: &str) -> rquickjs::Result<String> {
let value = ferrijs_fetch::headers::normalize_value(raw);
if ferrijs_fetch::headers::is_valid_value(&value) {
Ok(value)
} else {
Err(rquickjs::Exception::throw_type(ctx, "Invalid header value"))
}
}
}
#[rquickjs::methods]
impl HeadersJs {
#[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
pub fn to_string_tag() -> &'static str {
"Headers"
}
#[qjs(constructor)]
pub fn new<'js>(ctx: Ctx<'js>, init: Opt<Value<'js>>) -> rquickjs::Result<Self> {
let mut list = CoreHeaders::new();
if let Some(v) = init.0 {
if v.is_null() || v.is_number() {
return Err(rquickjs::Exception::throw_type(
&ctx,
"Failed to construct 'Headers': invalid init",
));
}
if !v.is_undefined() {
fill_header_list(Some(&ctx), &mut list, &v)?;
}
}
Ok(Self { list })
}
#[qjs(rename = "append")]
pub fn append(&mut self, ctx: Ctx<'_>, name: String, value: Coerced<String>) -> rquickjs::Result<()> {
let name = Self::check_name(&ctx, &name)?;
let value = Self::check_value(&ctx, &value.0)?;
self.list.append_combined(name, value);
Ok(())
}
#[qjs(rename = "set")]
pub fn set(&mut self, ctx: Ctx<'_>, name: String, value: Coerced<String>) -> rquickjs::Result<()> {
let name = Self::check_name(&ctx, &name)?;
let value = Self::check_value(&ctx, &value.0)?;
self.list.set(&name, value);
Ok(())
}
#[qjs(rename = "get")]
pub fn get<'js>(&self, ctx: Ctx<'js>, name: String) -> rquickjs::Result<Value<'js>> {
let name = Self::check_name(&ctx, &name)?;
match self.list.get_joined(&name) {
Some(joined) => joined.into_js(&ctx),
None => Ok(Value::new_null(ctx)),
}
}
#[qjs(rename = "getSetCookie")]
pub fn get_set_cookie(&self) -> Vec<String> {
self
.list
.get_set_cookie()
.into_iter()
.map(ToString::to_string)
.collect()
}
#[qjs(rename = "has")]
pub fn has(&self, ctx: Ctx<'_>, name: String) -> rquickjs::Result<bool> {
let name = Self::check_name(&ctx, &name)?;
Ok(self.list.contains(&name))
}
#[qjs(rename = "delete")]
pub fn delete(&mut self, ctx: Ctx<'_>, name: String) -> rquickjs::Result<()> {
let name = Self::check_name(&ctx, &name)?;
self.list.remove(&name);
Ok(())
}
#[qjs(rename = "entries")]
pub fn entries<'js>(ctx: Ctx<'js>, this: This<Class<'js, Self>>) -> rquickjs::Result<Object<'js>> {
live_iterator(&ctx, this.0, header_entry)
}
#[qjs(rename = "keys")]
pub fn keys<'js>(ctx: Ctx<'js>, this: This<Class<'js, Self>>) -> rquickjs::Result<Object<'js>> {
live_iterator(&ctx, this.0, header_key)
}
#[qjs(rename = "values")]
pub fn values<'js>(ctx: Ctx<'js>, this: This<Class<'js, Self>>) -> rquickjs::Result<Object<'js>> {
live_iterator(&ctx, this.0, header_val)
}
#[qjs(rename = PredefinedAtom::SymbolIterator)]
pub fn js_iterator<'js>(ctx: Ctx<'js>, this: This<Class<'js, Self>>) -> rquickjs::Result<Object<'js>> {
live_iterator(&ctx, this.0, header_entry)
}
#[qjs(rename = "forEach")]
pub fn for_each(&self, cb: rquickjs::Function<'_>) -> rquickjs::Result<()> {
for (name, value) in self.list.sorted_entries() {
cb.call::<_, ()>((value, name))?;
}
Ok(())
}
}
impl<'js> FetchResponseJs<'js> {
fn from_stream(
status: u16,
status_text: String,
url: String,
headers: Vec<(String, String)>,
redirected: bool,
type_: &'static str,
stream: ferrijs_fetch::ByteStream,
) -> Self {
Self {
status,
status_text,
url,
headers: CoreHeaders::from_pairs(headers),
body: Vec::new(),
redirected,
type_,
body_used: false,
net: Some(Arc::new(tokio::sync::Mutex::new(Some(stream)))),
body_stream: None,
}
}
fn opaque_redirect() -> Self {
Self {
status: 0,
status_text: String::new(),
url: String::new(),
headers: CoreHeaders::new(),
body: Vec::new(),
redirected: false,
type_: "opaqueredirect",
body_used: false,
net: None,
body_stream: None,
}
}
fn ensure_body_stream(&mut self, ctx: &Ctx<'js>) -> rquickjs::Result<Class<'js, ReadableStream<'js>>> {
if let Some(s) = &self.body_stream {
return Ok(s.clone());
}
let stream = match self.net.take() {
Some(net) => self::streams::from_net(ctx, net)?,
None => self::streams::from_bytes(ctx, std::mem::take(&mut self.body))?,
};
self.body_stream = Some(stream.clone());
Ok(stream)
}
async fn drain_stream(ctx: &Ctx<'js>, stream: Class<'js, ReadableStream<'js>>) -> rquickjs::Result<Vec<u8>> {
let obj = stream
.into_value()
.into_object()
.ok_or_else(|| rquickjs::Error::new_from_js_message("Response", "TypeError", "body is not a ReadableStream"))?;
let reader: Object<'js> = obj.get::<_, rquickjs::Function<'js>>("getReader")?.call((This(obj),))?;
let read: rquickjs::Function<'js> = reader.get("read")?;
let mut out = Vec::new();
loop {
let step: rquickjs::Promise<'js> = read.call((This(reader.clone()),))?;
let res: Object<'js> = step.into_future().await?;
if res.get::<_, bool>("done").unwrap_or(false) {
return Ok(out);
}
let chunk = chunk_bytes(&res.get::<_, Value<'js>>("value")?);
if out.len() + chunk.len() > MAX_FETCH_BODY_BYTES {
return Err(rquickjs::Exception::throw_type(
ctx,
&format!("response body exceeded {MAX_FETCH_BODY_BYTES} bytes"),
));
}
out.extend_from_slice(&chunk);
}
}
async fn consume(&mut self, ctx: &Ctx<'js>) -> rquickjs::Result<Vec<u8>> {
if self.body_used {
return Err(rquickjs::Exception::throw_type(ctx, "Body has already been consumed"));
}
if let Some(stream) = self.body_stream.clone() {
if stream.borrow().is_readable_stream_locked() {
return Err(rquickjs::Exception::throw_type(ctx, "Body is locked to a reader"));
}
self.body_used = true;
return match tokio::time::timeout(FETCH_BODY_DRAIN_TIMEOUT, Self::drain_stream(ctx, stream)).await {
Ok(r) => r,
Err(_) => Err(rquickjs::Exception::throw_type(ctx, "response body read timed out")),
};
}
self.body_used = true;
if let Some(net) = &self.net {
let mut guard = net.lock().await;
let mut out = Vec::new();
if let Some(body) = guard.as_mut() {
use futures::StreamExt as _;
let drained = tokio::time::timeout(FETCH_BODY_DRAIN_TIMEOUT, async {
while let Some(chunk) = body.next().await.transpose().map_err(|e| e.to_string())? {
if out.len() + chunk.len() > MAX_FETCH_BODY_BYTES {
return Err(format!("response body exceeded {MAX_FETCH_BODY_BYTES} bytes"));
}
out.extend_from_slice(&chunk);
}
Ok::<(), String>(())
})
.await;
*guard = None;
match drained {
Ok(Ok(())) => {},
Ok(Err(msg)) => return Err(rquickjs::Exception::throw_type(ctx, &msg)),
Err(_) => {
return Err(rquickjs::Exception::throw_type(ctx, "response body read timed out"));
},
}
return Ok(out);
}
*guard = None;
return Ok(out);
}
Ok(std::mem::take(&mut self.body))
}
}
impl<'js> BodyMixin<'js> for FetchResponseJs<'js> {
async fn consume_body(&mut self, ctx: &Ctx<'js>) -> rquickjs::Result<Vec<u8>> {
self.consume(ctx).await
}
fn content_type(&self) -> Option<String> {
self.headers.get_first("content-type").map(ToString::to_string)
}
}
impl<'js> BodyMixin<'js> for FetchRequestJs<'js> {
async fn consume_body(&mut self, ctx: &Ctx<'js>) -> rquickjs::Result<Vec<u8>> {
self.consume(ctx).await
}
fn content_type(&self) -> Option<String> {
self.headers.get_first("content-type").map(ToString::to_string)
}
}
pub(crate) trait BodyMixin<'js> {
fn consume_body(&mut self, ctx: &Ctx<'js>) -> impl Future<Output = rquickjs::Result<Vec<u8>>>;
fn content_type(&self) -> Option<String>;
async fn mixin_text(&mut self, ctx: &Ctx<'js>) -> rquickjs::Result<String> {
let b = self.consume_body(ctx).await?;
Ok(String::from_utf8_lossy(&b).into_owned())
}
async fn mixin_json(&mut self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
let b = self.consume_body(ctx).await?;
let v: serde_json::Value =
serde_json::from_slice(&b).map_err(|e| rquickjs::Error::new_from_js_message("json", "Error", e.to_string()))?;
json_to_js(ctx, &v)
}
async fn mixin_array_buffer(&mut self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
let b = self.consume_body(ctx).await?;
rquickjs::ArrayBuffer::new(ctx.clone(), b).map(rquickjs::ArrayBuffer::into_value)
}
async fn mixin_bytes(&mut self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
let b = self.consume_body(ctx).await?;
Ok(rquickjs::TypedArray::new(ctx.clone(), b)?.into_value())
}
async fn mixin_blob(&mut self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
let mime = self.content_type().unwrap_or_default();
let b = self.consume_body(ctx).await?;
Ok(Class::instance(ctx.clone(), Blob::from_bytes(ctx, b, Some(mime))?)?.into_value())
}
async fn mixin_form_data(&mut self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
let content_type = self.content_type().unwrap_or_default();
let boundary = ferrijs_fetch::multipart_boundary_of(&content_type);
let urlencoded = content_type
.split(';')
.next()
.is_some_and(|m| m.trim().eq_ignore_ascii_case("application/x-www-form-urlencoded"));
if boundary.is_none() && !urlencoded {
return Err(rquickjs::Exception::throw_type(
ctx,
&format!("Could not parse content as FormData: unsupported content type {content_type:?}"),
));
}
let bytes = self.consume_body(ctx).await?;
let form = match boundary {
Some(boundary) => self::multipart::form_data_from_fields(&ferrijs_fetch::parse_multipart(&bytes, &boundary)),
None => FormDataJs::from_urlencoded(&String::from_utf8_lossy(&bytes)),
};
Ok(Class::instance(ctx.clone(), form)?.into_value())
}
}
#[allow(unsafe_code)]
fn chunk_bytes(v: &Value<'_>) -> Vec<u8> {
if let Some(s) = v.as_string().and_then(|s| s.to_string().ok()) {
return s.into_bytes();
}
if let Ok(ta) = rquickjs::TypedArray::<u8>::from_value(v.clone()) {
let b: &[u8] = unsafe { ta.as_bytes() }.unwrap_or_default();
return b.to_vec();
}
if let Some(ab) = rquickjs::ArrayBuffer::from_value(v.clone())
&& let Some(b) = unsafe { ab.as_bytes() }
{
return b.to_vec();
}
Vec::new()
}
#[rquickjs::methods]
impl<'js> FetchResponseJs<'js> {
#[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
pub fn to_string_tag() -> &'static str {
"Response"
}
#[qjs(constructor)]
pub fn new(ctx: Ctx<'js>, body: Opt<Value<'js>>, init: Opt<Object<'js>>) -> rquickjs::Result<Self> {
let init = init.0;
let status = match init.as_ref().and_then(|o| o.get::<_, i64>("status").ok()) {
Some(s) if !(200..=599).contains(&s) => {
return Err(rquickjs::Exception::throw_range(
&ctx,
"Failed to construct 'Response': status is outside the range [200, 599]",
));
},
Some(s) => s as u16,
None => 200,
};
let status_text = init
.as_ref()
.and_then(|o| o.get::<_, String>("statusText").ok())
.unwrap_or_default();
let has_body = body.0.as_ref().is_some_and(|v| !v.is_null() && !v.is_undefined());
if has_body && matches!(status, 204 | 205 | 304) {
return Err(rquickjs::Exception::throw_type(
&ctx,
"Failed to construct 'Response': Response with null body status cannot have body",
));
}
let extracted = match &body.0 {
Some(v) => extract_body(&ctx, v)?,
None => None,
};
let headers = init_headers(init.as_ref(), extracted.as_ref());
let (bytes, body_stream) = match extracted.map(|e| e.source) {
Some(BodySource::Bytes(b)) => (b, None),
Some(BodySource::Stream(s)) => (Vec::new(), Some(s)),
None => (Vec::new(), None),
};
Ok(Self {
status,
status_text,
url: String::new(),
headers,
body: bytes,
redirected: false,
type_: "default",
body_used: false,
net: None,
body_stream,
})
}
#[qjs(static, rename = "json")]
pub fn json_static(ctx: Ctx<'js>, data: Value<'js>, init: Opt<Object<'js>>) -> rquickjs::Result<Self> {
let init = init.0;
let json: serde_json::Value = crate::value::serde_from_js(&ctx, data)?;
let status = init
.as_ref()
.and_then(|o| o.get::<_, i64>("status").ok())
.unwrap_or(200) as u16;
let status_text = init
.as_ref()
.and_then(|o| o.get::<_, String>("statusText").ok())
.unwrap_or_default();
Ok(Self {
status,
status_text,
url: String::new(),
headers: init_headers(
init.as_ref(),
Some(&ExtractedBody {
source: BodySource::Bytes(Vec::new()),
content_type: Some("application/json".to_string()),
forced: false,
}),
),
body: json.to_string().into_bytes(),
redirected: false,
type_: "default",
body_used: false,
net: None,
body_stream: None,
})
}
#[qjs(static, rename = "error")]
pub fn error() -> Self {
Self {
status: 0,
status_text: String::new(),
url: String::new(),
headers: CoreHeaders::new(),
body: Vec::new(),
redirected: false,
type_: "error",
body_used: false,
net: None,
body_stream: None,
}
}
#[qjs(static, rename = "redirect")]
pub fn redirect(ctx: Ctx<'_>, url: String, status: Opt<i64>) -> rquickjs::Result<Self> {
let status = status.0.unwrap_or(302);
if ![301, 302, 303, 307, 308].contains(&status) {
return Err(rquickjs::Exception::throw_range(&ctx, "Invalid redirect status code"));
}
Ok(Self {
status: status as u16,
status_text: String::new(),
url: String::new(),
headers: CoreHeaders::from_pairs(vec![("location".to_string(), url)]),
body: Vec::new(),
redirected: false,
type_: "default",
body_used: false,
net: None,
body_stream: None,
})
}
#[qjs(get, rename = "status")]
pub fn status(&self) -> u16 {
self.status
}
#[qjs(get, rename = "ok")]
pub fn ok(&self) -> bool {
(200..300).contains(&self.status)
}
#[qjs(get, rename = "statusText")]
pub fn status_text(&self) -> String {
self.status_text.clone()
}
#[qjs(get, rename = "url")]
pub fn url(&self) -> String {
self.url.clone()
}
#[qjs(get, rename = "redirected")]
pub fn redirected(&self) -> bool {
self.redirected
}
#[qjs(get, rename = "type")]
pub fn type_(&self) -> String {
self.type_.to_string()
}
#[qjs(get, rename = "bodyUsed")]
pub fn body_used(&self) -> bool {
self.body_used
}
#[qjs(get, rename = "headers")]
pub fn headers(&self, ctx: Ctx<'js>) -> rquickjs::Result<Class<'js, HeadersJs>> {
Class::instance(ctx, HeadersJs::from_pairs(self.headers.iter().cloned()))
}
#[qjs(get, rename = "body")]
pub fn body(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Class<'js, ReadableStream<'js>>> {
self.ensure_body_stream(&ctx)
}
#[qjs(rename = "text")]
pub async fn text(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<String> {
self.mixin_text(&ctx).await
}
#[qjs(rename = "json")]
pub async fn json(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
self.mixin_json(&ctx).await
}
#[qjs(rename = "arrayBuffer")]
pub async fn array_buffer(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
self.mixin_array_buffer(&ctx).await
}
#[qjs(rename = "bytes")]
pub async fn bytes(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
self.mixin_bytes(&ctx).await
}
#[qjs(rename = "blob")]
pub async fn blob(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
self.mixin_blob(&ctx).await
}
#[qjs(rename = "formData")]
pub async fn form_data(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
self.mixin_form_data(&ctx).await
}
#[qjs(rename = "clone")]
pub fn clone_(this: This<Class<'js, Self>>, ctx: Ctx<'js>) -> rquickjs::Result<Self> {
let (branch1, branch2) = {
let mut me = this.borrow_mut();
if me.body_used {
return Err(rquickjs::Exception::throw_type(&ctx, "Cannot clone a used Response"));
}
let stream = me.ensure_body_stream(&ctx)?;
drop(me);
ferrijs_std::stream_web::tee_readable_stream(ctx.clone(), stream)?
};
let mut me = this.borrow_mut();
me.body_stream = Some(branch1);
Ok(Self {
status: me.status,
status_text: me.status_text.clone(),
url: me.url.clone(),
headers: me.headers.clone(),
body: Vec::new(),
redirected: me.redirected,
type_: me.type_,
body_used: false,
net: None,
body_stream: Some(branch2),
})
}
}
impl<'js> FetchRequestJs<'js> {
fn ensure_body_stream(&mut self, ctx: &Ctx<'js>) -> rquickjs::Result<Class<'js, ReadableStream<'js>>> {
if let Some(s) = &self.body_stream {
return Ok(s.clone());
}
let stream = self::streams::from_bytes(ctx, self.body.clone())?;
self.body_stream = Some(stream.clone());
Ok(stream)
}
fn body_is_disturbed(&self) -> bool {
if self.body_used {
return true;
}
self.body_stream.as_ref().is_some_and(|s| {
let s = s.borrow();
s.disturbed || s.is_readable_stream_locked()
})
}
async fn consume(&mut self, ctx: &Ctx<'js>) -> rquickjs::Result<Vec<u8>> {
if self.body_used {
return Err(rquickjs::Exception::throw_type(ctx, "Body has already been consumed"));
}
if let Some(stream) = self.body_stream.clone() {
if stream.borrow().is_readable_stream_locked() {
return Err(rquickjs::Exception::throw_type(ctx, "Body is locked to a reader"));
}
self.body_used = true;
return match tokio::time::timeout(FETCH_BODY_DRAIN_TIMEOUT, FetchResponseJs::drain_stream(ctx, stream)).await {
Ok(r) => r,
Err(_) => Err(rquickjs::Exception::throw_type(ctx, "request body read timed out")),
};
}
self.body_used = true;
Ok(std::mem::take(&mut self.body))
}
}
#[rquickjs::methods]
impl<'js> FetchRequestJs<'js> {
#[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
pub fn to_string_tag() -> &'static str {
"Request"
}
#[qjs(constructor)]
pub fn new(ctx: Ctx<'js>, input: Value<'js>, init: Opt<Object<'js>>) -> rquickjs::Result<Self> {
let init = init.0;
let mut req = if let Ok(other) = Class::<FetchRequestJs<'js>>::from_value(&input) {
let o = other.borrow();
Self {
url: o.url.clone(),
method: o.method.clone(),
headers: o.headers.clone(),
body: o.body.clone(),
redirect: o.redirect.clone(),
credentials: o.credentials.clone(),
body_used: false,
cache: o.cache.clone(),
mode: o.mode.clone(),
referrer: o.referrer.clone(),
referrer_policy: o.referrer_policy.clone(),
integrity: o.integrity.clone(),
keepalive: o.keepalive,
destination: o.destination.clone(),
duplex: o.duplex.clone(),
signal_inner: o.signal_inner.clone(),
signal: o.signal.clone(),
body_stream: None,
}
} else {
Self {
url: input.as_string().and_then(|s| s.to_string().ok()).unwrap_or_default(),
method: "GET".to_string(),
headers: CoreHeaders::new(),
body: Vec::new(),
redirect: "follow".to_string(),
credentials: "same-origin".to_string(),
body_used: false,
cache: "default".to_string(),
mode: "cors".to_string(),
referrer: "about:client".to_string(),
referrer_policy: String::new(),
integrity: String::new(),
keepalive: false,
destination: String::new(),
duplex: None,
signal_inner: None,
signal: None,
body_stream: None,
}
};
if let Some(o) = init.as_ref() {
if let Ok(m) = o.get::<_, String>("method") {
req.method = m.to_ascii_uppercase();
}
if let Ok(r) = o.get::<_, String>("redirect") {
req.redirect = r;
}
if let Ok(c) = o.get::<_, String>("credentials") {
req.credentials = c;
}
if let Ok(v) = o.get::<_, String>("cache") {
req.cache = v;
}
if let Ok(v) = o.get::<_, String>("mode") {
req.mode = v;
}
if let Ok(v) = o.get::<_, String>("referrer") {
req.referrer = v;
}
if let Ok(v) = o.get::<_, String>("referrerPolicy") {
req.referrer_policy = v;
}
if let Ok(v) = o.get::<_, String>("integrity") {
req.integrity = v;
}
if let Ok(v) = o.get::<_, bool>("keepalive") {
req.keepalive = v;
}
if let Ok(v) = o.get::<_, String>("duplex") {
req.duplex = Some(v);
}
if let Ok(sig) = o.get::<_, Value<'js>>("signal")
&& let Ok(s) = Class::<AbortSignal<'js>>::from_value(&sig)
{
req.signal_inner = Some(self::abort::native_channel(&ctx, &s)?);
req.signal = Some(s);
}
let extracted = match o.get::<_, Value<'_>>("body").ok() {
Some(v) => extract_body(&ctx, &v)?,
None => None,
};
if extracted.is_some() && matches!(req.method.as_str(), "GET" | "HEAD") {
return Err(rquickjs::Exception::throw_type(
&ctx,
"Failed to construct 'Request': Request with GET/HEAD method cannot have body.",
));
}
match extracted.as_ref().map(|e| &e.source) {
Some(BodySource::Bytes(bytes)) if !bytes.is_empty() => req.body.clone_from(bytes),
Some(BodySource::Stream(stream)) => {
req.body = Vec::new();
req.body_stream = Some(stream.clone());
},
_ => {},
}
req.headers = {
let mut h = init_headers(init.as_ref(), extracted.as_ref());
if h.is_empty() {
std::mem::take(&mut req.headers)
} else {
if let Ok(existing) = Class::<FetchRequestJs<'js>>::from_value(&input) {
for (name, value) in existing.borrow().headers.iter() {
h.set_if_absent(name, value.clone());
}
}
h
}
};
}
Ok(req)
}
#[qjs(get, rename = "url")]
pub fn url(&self) -> String {
self.url.clone()
}
#[qjs(get, rename = "method")]
pub fn method(&self) -> String {
self.method.clone()
}
#[qjs(get, rename = "redirect")]
pub fn redirect(&self) -> String {
self.redirect.clone()
}
#[qjs(get, rename = "credentials")]
pub fn credentials(&self) -> String {
self.credentials.clone()
}
#[qjs(get, rename = "bodyUsed")]
pub fn body_used(&self) -> bool {
self.body_used
}
#[qjs(get, rename = "cache")]
pub fn cache(&self) -> String {
self.cache.clone()
}
#[qjs(get, rename = "mode")]
pub fn mode(&self) -> String {
self.mode.clone()
}
#[qjs(get, rename = "referrer")]
pub fn referrer(&self) -> String {
self.referrer.clone()
}
#[qjs(get, rename = "referrerPolicy")]
pub fn referrer_policy(&self) -> String {
self.referrer_policy.clone()
}
#[qjs(get, rename = "integrity")]
pub fn integrity(&self) -> String {
self.integrity.clone()
}
#[qjs(get, rename = "keepalive")]
pub fn keepalive(&self) -> bool {
self.keepalive
}
#[qjs(get, rename = "destination")]
pub fn destination(&self) -> String {
self.destination.clone()
}
#[qjs(get, rename = "duplex")]
pub fn duplex(&self) -> Option<String> {
self.duplex.clone()
}
#[qjs(get, rename = "isHistoryNavigation")]
pub fn is_history_navigation(&self) -> bool {
false
}
#[qjs(get, rename = "isReloadNavigation")]
pub fn is_reload_navigation(&self) -> bool {
false
}
#[qjs(get, rename = "headers")]
pub fn headers(&self, ctx: Ctx<'js>) -> rquickjs::Result<Class<'js, HeadersJs>> {
Class::instance(ctx, HeadersJs::from_pairs(self.headers.iter().cloned()))
}
#[qjs(get, rename = "signal")]
pub fn signal(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Class<'js, AbortSignal<'js>>> {
if let Some(s) = &self.signal {
return Ok(s.clone());
}
let fresh = self::abort::fresh_instance(&ctx)?;
self.signal = Some(fresh.clone());
Ok(fresh)
}
#[qjs(get, rename = "body")]
pub fn body(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Class<'js, ReadableStream<'js>>> {
self.ensure_body_stream(&ctx)
}
#[qjs(rename = "text")]
pub async fn text(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<String> {
self.mixin_text(&ctx).await
}
#[qjs(rename = "json")]
pub async fn json(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
self.mixin_json(&ctx).await
}
#[qjs(rename = "arrayBuffer")]
pub async fn array_buffer(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
self.mixin_array_buffer(&ctx).await
}
#[qjs(rename = "bytes")]
pub async fn bytes(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
self.mixin_bytes(&ctx).await
}
#[qjs(rename = "blob")]
pub async fn blob(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
self.mixin_blob(&ctx).await
}
#[qjs(rename = "formData")]
pub async fn form_data(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
self.mixin_form_data(&ctx).await
}
#[qjs(rename = "clone")]
pub fn clone_(this: This<Class<'js, Self>>, ctx: Ctx<'js>) -> rquickjs::Result<Self> {
{
let me = this.borrow();
if me.body_used {
return Err(rquickjs::Exception::throw_type(&ctx, "Cannot clone a used Request"));
}
}
let branch2 = if this.borrow().body_stream.is_some() {
let stream = this.borrow_mut().ensure_body_stream(&ctx)?;
let (branch1, branch2) = ferrijs_std::stream_web::tee_readable_stream(ctx.clone(), stream)?;
this.borrow_mut().body_stream = Some(branch1);
Some(branch2)
} else {
None
};
let me = this.borrow();
Ok(Self {
url: me.url.clone(),
method: me.method.clone(),
headers: me.headers.clone(),
body: me.body.clone(),
redirect: me.redirect.clone(),
credentials: me.credentials.clone(),
body_used: false,
cache: me.cache.clone(),
mode: me.mode.clone(),
referrer: me.referrer.clone(),
referrer_policy: me.referrer_policy.clone(),
integrity: me.integrity.clone(),
keepalive: me.keepalive,
destination: me.destination.clone(),
duplex: me.duplex.clone(),
signal_inner: me.signal_inner.clone(),
signal: me.signal.clone(),
body_stream: branch2,
})
}
}
pub fn define_classes<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<()> {
let g = ctx.globals();
Class::<HeadersJs>::define(&g)?;
Class::<FetchResponseJs<'js>>::define(&g)?;
Class::<FetchRequestJs<'js>>::define(&g)?;
Ok(())
}
pub fn install(ctx: &Ctx<'_>, backend: Arc<dyn FetchBackend>) -> rquickjs::Result<()> {
define_classes(ctx)?;
let f = function(ctx, backend)?;
ctx.globals().set("fetch", f)?;
Ok(())
}
pub fn function<'js>(ctx: &Ctx<'js>, backend: Arc<dyn FetchBackend>) -> rquickjs::Result<rquickjs::Function<'js>> {
rquickjs::Function::new(ctx.clone(), move |ctx, input, init| {
do_fetch(ctx, input, init, backend.clone())
})
}
pub struct FetchExtension {
backend: Arc<dyn FetchBackend>,
}
impl FetchExtension {
#[must_use]
pub fn new(backend: Arc<dyn FetchBackend>) -> Self {
Self { backend }
}
}
impl Default for FetchExtension {
fn default() -> Self {
Self::new(Arc::new(Client::new()))
}
}
impl crate::extension::Extension for FetchExtension {
fn name(&self) -> &'static str {
"fetch"
}
fn install(&self, ctx: &Ctx<'_>) -> rquickjs::Result<()> {
install(ctx, Arc::clone(&self.backend))
}
}
fn do_fetch<'js>(
ctx: Ctx<'js>,
input: Value<'js>,
init: Opt<Object<'js>>,
backend: Arc<dyn FetchBackend>,
) -> rquickjs::Result<Value<'js>> {
{
let req = Class::<FetchRequestJs<'js>>::from_value(&input).ok();
if let Some(r) = req.as_ref()
&& r.borrow().body_is_disturbed()
{
return Err(rquickjs::Exception::throw_type(
&ctx,
"Cannot fetch a Request whose body has already been read",
));
}
let url = req
.as_ref()
.map(|r| r.borrow().url.clone())
.or_else(|| input.as_string().and_then(|s| s.to_string().ok()))
.or_else(|| input.as_object().and_then(|o| o.get::<_, String>("url").ok()))
.unwrap_or_default();
let net_guard = ferrijs_std::permissions::container(&ctx).map(|container| NetGuard {
policy: Some(backend.net_policy(&ctx, container)),
block_metadata: true,
block_private: false,
});
let init = init.0;
let method = init
.as_ref()
.and_then(|o| o.get::<_, String>("method").ok())
.or_else(|| req.as_ref().map(|r| r.borrow().method.clone()));
let mut header_list: CoreHeaders = init
.as_ref()
.and_then(|o| o.get::<_, Value<'_>>("headers").ok())
.map(|v| header_list_from(&v))
.or_else(|| req.as_ref().map(|r| r.borrow().headers.clone()))
.unwrap_or_default();
let extracted = match init.as_ref().and_then(|o| o.get::<_, Value<'_>>("body").ok()) {
Some(v) => extract_body(&ctx, &v)?,
None => None,
};
apply_body_content_type(&mut header_list, extracted.as_ref());
let body_source = match extracted.map(|e| e.source) {
Some(source) => Some(source),
None => match req.as_ref().map(|r| r.borrow().body.clone()) {
Some(b) if !b.is_empty() => Some(BodySource::Bytes(b)),
_ => None,
},
};
let headers = header_list.into_pairs();
let body = match body_source {
None => ferrijs_fetch::Body::empty(),
Some(BodySource::Bytes(bytes)) => ferrijs_fetch::Body::from_bytes(bytes),
Some(BodySource::Stream(stream)) => {
ferrijs_fetch::Body::from_stream(self::streams::to_byte_stream(&ctx, stream)?)
},
};
let redirect = init
.as_ref()
.and_then(|o| o.get::<_, String>("redirect").ok())
.or_else(|| req.as_ref().map(|r| r.borrow().redirect.clone()));
let redirect = match redirect.as_deref() {
Some("manual") => RedirectMode::Manual,
Some("error") => RedirectMode::Error,
_ => RedirectMode::Follow,
};
let credentials = init
.as_ref()
.and_then(|o| o.get::<_, String>("credentials").ok())
.or_else(|| req.as_ref().map(|r| r.borrow().credentials.clone()));
let credentials = match credentials.as_deref() {
Some("omit") => Credentials::Omit,
Some("include") => Credentials::Include,
_ => Credentials::SameOrigin,
};
let signal = init
.as_ref()
.and_then(|o| o.get::<_, Value<'_>>("signal").ok())
.and_then(|v| Class::<AbortSignal<'js>>::from_value(&v).ok())
.and_then(|s| self::abort::native_channel(&ctx, &s).ok())
.or_else(|| req.as_ref().and_then(|r| r.borrow().signal_inner.clone()));
let failure_ctx = ctx.clone();
let promised = rquickjs::promise::Promised::from(async move {
let fail = |e: ferrijs_fetch::FetchError| fetch_failure(&failure_ctx, e);
let request = FetchRequest {
url: url.clone(),
method: method.unwrap_or_else(|| "GET".to_string()),
headers,
body,
redirect,
credentials,
net_guard,
timeout: None,
};
if let Some(sig) = &signal
&& sig.is_aborted()
{
return Err(rquickjs::Error::new_from_js_message(
"fetch",
"AbortError",
sig.reason_message(),
));
}
let fut = backend.fetch(request);
let resp = match &signal {
Some(sig) => {
tokio::select! {
r = fut => r.map_err(fail)?,
() = sig.aborted() => {
return Err(fail(ferrijs_fetch::FetchError::Abort(sig.reason_message())));
}
}
},
None => fut.await.map_err(fail)?,
};
if resp.unfollowed_redirect {
return Ok(FetchResponseJs::opaque_redirect());
}
let ferrijs_fetch::Response {
status,
status_text,
url,
headers,
body,
redirected,
type_,
..
} = resp;
let out = FetchResponseJs::from_stream(
status,
status_text,
url,
headers.entries().to_vec(),
redirected,
type_.as_str(),
body.into_stream(),
);
Ok::<_, rquickjs::Error>(out)
});
promised.into_js(&ctx)
}
}
fn fetch_failure(ctx: &Ctx<'_>, error: ferrijs_fetch::FetchError) -> rquickjs::Error {
match error {
ferrijs_fetch::FetchError::Denied(denied) => ferrijs_std::permissions::throw_denied(ctx, &denied),
ferrijs_fetch::FetchError::Abort(reason) => rquickjs::Error::new_from_js_message("fetch", "AbortError", reason),
other => rquickjs::Error::new_from_js_message("fetch", "TypeError", other.to_string()),
}
}