use crate::fetch::headers::JsHeaders;
use crate::fetch::request::{JsRequest, RequestInit};
use crate::fetch::response::JsResponse;
use boa_engine::class::Class;
use boa_engine::realm::Realm;
use boa_engine::{
Context, Finalize, JsData, JsError, JsObject, JsResult, JsString, JsValue, NativeObject, Trace,
boa_module, js_error,
};
use either::Either;
use http::{HeaderName, HeaderValue, Request as HttpRequest, Request};
use std::cell::RefCell;
use std::rc::Rc;
pub mod headers;
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,
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())
}
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 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"));
};
request_ref.data().inner().clone()
}
};
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), context).await?;
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;
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,
)
}
}
#[doc(inline)]
pub use js_module::fetch;
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, context)?;
Ok(())
}