use crate::fnc::script::{
fetch::{
body::{Body, BodyData, BodyKind},
classes::{self, Request, RequestInit, Response, ResponseInit, ResponseType},
RequestError,
},
modules::surrealdb::query::{QueryContext, QUERY_DATA_PROP_NAME},
};
use futures::TryStreamExt;
use js::{class::OwnedBorrow, function::Opt, Class, Ctx, Exception, Result, Value};
use reqwest::{
header::{HeaderValue, CONTENT_TYPE},
redirect, Body as ReqBody,
};
use std::sync::Arc;
use super::classes::Headers;
#[js::function]
#[allow(unused_variables)]
pub async fn fetch<'js>(
ctx: Ctx<'js>,
input: Value<'js>,
init: Opt<RequestInit<'js>>,
) -> Result<Response<'js>> {
let js_req = Request::new(ctx.clone(), input, init)?;
let url = js_req.url;
if ctx.globals().contains_key(QUERY_DATA_PROP_NAME)? {
let query_ctx =
ctx.globals().get::<_, OwnedBorrow<'js, QueryContext<'js>>>(QUERY_DATA_PROP_NAME)?;
query_ctx
.context
.check_allowed_net(&url)
.map_err(|e| Exception::throw_message(&ctx, &e.to_string()))?;
} else {
#[cfg(debug_assertions)]
panic!("Trying to fetch a URL but no QueryContext is present. QueryContext is required for checking if the URL is allowed to be fetched.")
}
let req = reqwest::Request::new(js_req.init.method, url.clone());
let headers = js_req.init.headers;
let headers = headers.borrow();
let mut headers = headers.inner.clone();
let redirect = js_req.init.request_redirect;
let policy = redirect::Policy::custom(move |attempt| {
match redirect {
classes::RequestRedirect::Follow => {
if attempt.previous().len() > 20 {
attempt.error("too many redirects")
} else {
attempt.follow()
}
}
classes::RequestRedirect::Error => attempt.error("unexpected redirect"),
classes::RequestRedirect::Manual => attempt.stop(),
}
});
let client = reqwest::Client::builder().redirect(policy).build().map_err(|e| {
Exception::throw_internal(&ctx, &format!("Could not initialize http client: {e}"))
})?;
let mut req_builder = reqwest::RequestBuilder::from_parts(client, req);
if let Some(body) = js_req.init.body {
match body.data.replace(BodyData::Used) {
BodyData::Stream(x) => {
let body = ReqBody::wrap_stream(x.into_inner());
req_builder = req_builder.body(body);
}
BodyData::Buffer(x) => {
let body = ReqBody::from(x);
req_builder = req_builder.body(body);
}
BodyData::Used => return Err(Exception::throw_type(&ctx, "Body unusable")),
};
match body.kind {
BodyKind::Buffer => {}
BodyKind::String => {
headers
.entry(CONTENT_TYPE)
.or_insert_with(|| HeaderValue::from_static("text/plain;charset=UTF-8"));
}
BodyKind::Blob(mime) => {
if let Ok(x) = HeaderValue::from_bytes(mime.as_bytes()) {
headers.entry(CONTENT_TYPE).or_insert_with(|| x);
}
}
}
}
let response = req_builder
.headers(headers)
.send()
.await
.map_err(|e| Exception::throw_type(&ctx, &e.to_string()))?;
let headers = Headers::from_map(response.headers().clone());
let headers = Class::instance(ctx, headers)?;
let init = ResponseInit {
headers,
status: response.status().as_u16(),
status_text: response.status().canonical_reason().unwrap_or("").to_owned(),
};
let body = Body::stream(
BodyKind::Buffer,
response.bytes_stream().map_err(Arc::new).map_err(RequestError::Reqwest),
);
let response = Response {
body,
init,
url: Some(url),
r#type: ResponseType::Default,
was_redirected: false,
};
Ok(response)
}