use std::{collections::HashMap, str::FromStr};
use futures::StreamExt;
use hashira::{
app::AppService,
web::{
header::{HeaderName, HeaderValue},
method::Method,
uri::Uri,
Body, Request, Response,
},
};
use wasm_bindgen::{JsCast, JsError, JsValue, UnwrapThrowExt};
use web_sys::ResponseInit;
#[allow(clippy::let_and_return)]
pub async fn handle_request(
service: AppService,
web_req: web_sys::Request,
) -> Result<web_sys::Response, JsError> {
let req = crate::core::map_request(web_req).await?;
let res = service.handle(req).await;
let web_res = crate::core::map_response(res).await?;
Ok(web_res)
}
async fn map_request(web_req: web_sys::Request) -> Result<Request, JsError> {
let method = Method::from_str(&web_req.method()).expect("invalid method");
let uri = Uri::from_str(&web_req.url()).expect("invalid uri");
let mut builder = Request::builder().method(method).uri(uri);
let req_headers = web_req
.headers()
.unchecked_into::<super::bindings::headers::Headers>();
let header_entries = req_headers.entries();
let iterator = js_sys::try_iter(header_entries.as_ref())
.map_err(map_js_error("failed to get headers iterator"))?;
if let Some(iterator) = iterator {
for array in iterator {
let array: JsValue =
array.map_err(map_js_error("failed to convert headers to array"))?;
let array = array
.dyn_into::<js_sys::Array>()
.expect("failed to cast header to array");
let key_str = array.at(0).as_string().unwrap();
let value_str = array.at(1).as_string().unwrap();
let key = HeaderName::from_str(&key_str)?;
let value = HeaderValue::from_str(&value_str)?;
builder = builder.header(key, value);
}
}
let bytes = match web_req.body() {
Some(s) => {
let readable = s.dyn_into().unwrap(); let mut stream = wasm_streams::ReadableStream::from_raw(readable).into_stream();
let mut bytes = vec![];
while let Some(js) = stream.next().await {
let chunk = js.map_err(map_js_error("invalid chunk"))?;
let chunk_str = chunk
.as_string()
.expect_throw("failed to convert chunk to string");
bytes.extend(chunk_str.as_bytes());
}
Body::from(bytes)
}
None => Body::empty(),
};
let req = builder.body(bytes)?;
Ok(req)
}
async fn map_response(res: Response) -> Result<web_sys::Response, JsError> {
let (parts, body) = res.into_parts();
let body = body
.into_bytes()
.await
.map_err(|err| JsError::new(&err.to_string()))?;
let mut bytes = body.to_vec();
let mut init = ResponseInit::new();
init.status(parts.status.as_u16());
let mut map = HashMap::new();
for key in parts.headers.keys() {
let name = key.to_string();
let values = parts
.headers
.get_all(&name)
.iter()
.filter_map(|n| n.to_str().ok())
.map(|s| s.to_owned())
.collect::<Vec<_>>()
.join(";");
map.insert(name, values);
}
let headers = serde_wasm_bindgen::to_value(&map)?;
init.headers(&headers);
let res = web_sys::Response::new_with_opt_u8_array_and_init(Some(&mut bytes), &init)
.map_err(map_js_error("failed to create response"))?;
Ok(res)
}
fn map_js_error(details: impl Into<String>) -> impl FnOnce(JsValue) -> JsError {
fn js_error(details: &str, js_error: JsValue) -> JsError {
if js_error.is_string() {
return JsError::new(&js_error.as_string().unwrap());
}
let error = serde_wasm_bindgen::from_value::<String>(js_error)
.expect("failed to convert js to string");
let message = format!("{details}, {error}");
JsError::new(&message)
}
let details = details.into();
move |err| {
let s = details.as_str();
js_error(s, err)
}
}