use crate::fetch::headers::JsHeaders;
use crate::fetch::headers_iterator::{HeadersIterator, IterationKind};
use crate::fetch::request::{JsRequest, RequestInit};
use crate::fetch::response::JsResponse;
use boa_engine::class::Class;
use boa_engine::object::FunctionObjectBuilder;
use boa_engine::property::PropertyDescriptor;
use boa_engine::realm::Realm;
use boa_engine::{
Context, Finalize, JsData, JsError, JsObject, JsResult, JsString, JsSymbol, JsValue,
NativeObject, Trace, boa_module, js_error, js_string, native_function::NativeFunction,
};
use either::Either;
use http::{HeaderName, HeaderValue, Request as HttpRequest, Request};
use std::cell::RefCell;
use std::rc::Rc;
pub mod headers;
pub mod headers_iterator;
pub mod request;
pub mod response;
pub mod tests;
mod fetchers;
#[doc(inline)]
pub use fetchers::*;
pub trait Fetcher: NativeObject {
fn resolve_uri(&self, uri: String, _context: &mut Context) -> JsResult<String> {
Ok(uri)
}
#[expect(async_fn_in_trait, reason = "all our APIs are single-threaded")]
async fn fetch(
self: Rc<Self>,
request: JsRequest,
signal: Option<JsObject>,
context: &RefCell<&mut Context>,
) -> JsResult<JsResponse>;
}
#[derive(Debug, Trace, Finalize, JsData)]
struct FetcherRc<T: Fetcher>(#[unsafe_ignore_trace] pub Rc<T>);
impl<T: Fetcher> Clone for FetcherRc<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
fn get_fetcher<T: Fetcher>(context: &mut Context) -> JsResult<Rc<T>> {
let Some(fetcher) = context.get_data::<FetcherRc<T>>().cloned().or_else(|| {
context
.realm()
.host_defined()
.get::<FetcherRc<T>>()
.cloned()
}) else {
return Err(
js_error!(Error: "Implementation of fetch requires a fetcher registered in the context"),
);
};
Ok(fetcher.0.clone())
}
fn check_abort(signal: Option<&JsObject>, context: &mut Context) -> JsResult<()> {
if let Some(signal_obj) = signal
&& let Some(signal_ref) = signal_obj.downcast_ref::<crate::abort::JsAbortSignal>()
&& signal_ref.is_aborted()
{
return Err(JsError::from_opaque(signal_ref.abort_reason(context)));
}
Ok(())
}
async fn fetch_inner<T: Fetcher>(
resource: Either<JsString, JsObject>,
options: Option<RequestInit>,
context: &RefCell<&mut Context>,
) -> JsResult<JsValue> {
let fetcher = get_fetcher::<T>(&mut context.borrow_mut())?;
let (options, signal) = match options {
Some(mut opts) => {
let sig = opts.take_signal();
(Some(opts), sig)
}
None => (None, None),
};
check_abort(signal.as_ref(), &mut context.borrow_mut())?;
let mut signal = signal;
let request: Request<Vec<u8>> = match resource {
Either::Left(url) => {
let url = url.to_std_string().map_err(JsError::from_rust)?;
let url = fetcher
.resolve_uri(url, &mut context.borrow_mut())
.map_err(JsError::from_rust)?;
let r = HttpRequest::get(url).body(Vec::new());
r.map_err(JsError::from_rust)?
}
Either::Right(request) => {
let Ok(request) = request.downcast::<JsRequest>() else {
return Err(js_error!(TypeError: "Resource must be a URL or Request object"));
};
let Ok(request_ref) = request.try_borrow() else {
return Err(js_error!(TypeError: "Request object is already in use"));
};
signal = signal.or_else(|| request_ref.data().signal());
request_ref.data().inner().clone()
}
};
check_abort(signal.as_ref(), &mut context.borrow_mut())?;
let mut request = if let Some(options) = options {
options.into_request_builder(Some(request))?
} else {
request
};
if !request.headers().contains_key(
"accept-language"
.parse::<HeaderName>()
.map_err(JsError::from_rust)?,
) {
let lang = HeaderValue::from_static("en-US");
request.headers_mut().append("Accept-Language", lang);
}
let response = fetcher
.fetch(JsRequest::from(request), signal.clone(), context)
.await?;
check_abort(signal.as_ref(), &mut context.borrow_mut())?;
let result = Class::from_data(response, &mut context.borrow_mut())?;
Ok(result.into())
}
#[boa_module]
pub mod js_module {
use crate::fetch::request::RequestInit;
use crate::fetch::{Fetcher, fetch_inner};
use boa_engine::object::builtins::JsPromise;
use boa_engine::{Context, JsObject, JsString};
use either::Either;
type JsHeaders = super::JsHeaders;
type JsRequest = super::JsRequest;
type JsResponse = super::JsResponse;
type HeadersIterator = super::headers_iterator::HeadersIterator;
pub fn fetch<T: Fetcher>(
resource: Either<JsString, JsObject>,
options: Option<RequestInit>,
context: &mut Context,
) -> JsPromise {
JsPromise::from_async_fn(
async move |context| fetch_inner::<T>(resource, options, context).await,
context,
)
}
}
fn headers_symbol_iterator(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let this_object = this.as_object().ok_or_else(
|| js_error!(TypeError: "`Headers.prototype[Symbol.iterator]` requires a `Headers` object"),
)?;
let Ok(headers) = this_object.clone().downcast::<JsHeaders>() else {
return Err(
js_error!(TypeError: "`Headers.prototype[Symbol.iterator]` requires a `Headers` object"),
);
};
HeadersIterator::create_headers_iterator(headers, IterationKind::KeyAndValue, context)
}
pub fn register<F: Fetcher>(
fetcher: F,
realm: Option<Realm>,
context: &mut Context,
) -> JsResult<()> {
if let Some(ref realm) = realm {
realm.host_defined_mut().insert(FetcherRc(Rc::new(fetcher)));
} else {
context.insert_data(FetcherRc(Rc::new(fetcher)));
}
js_module::boa_register::<F>(realm.clone(), context)?;
let headers_proto = match realm {
Some(realm) => realm.get_class::<JsHeaders>(),
None => context.get_global_class::<JsHeaders>(),
}
.ok_or_else(|| js_error!(Error: "Headers class should be registered"))?
.prototype();
let iterator = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_fn_ptr(headers_symbol_iterator),
)
.name(js_string!("[Symbol.iterator]"))
.length(0)
.constructor(false)
.build();
headers_proto.define_property_or_throw(
JsSymbol::iterator(),
PropertyDescriptor::builder()
.value(iterator)
.writable(true)
.enumerable(false)
.configurable(true),
context,
)?;
Ok(())
}