use crate::error::{Error, Result};
use crate::request::{Redirect, Request};
use crate::response::Response;
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = fetch)]
fn fetch_with_request(input: &web_sys::Request) -> js_sys::Promise;
}
pub(crate) async fn send(request: Request) -> Result<Response> {
let init = web_sys::RequestInit::new();
init.set_method(request.method.as_str());
init.set_redirect(match request.redirect {
Redirect::Follow => web_sys::RequestRedirect::Follow,
Redirect::Manual => web_sys::RequestRedirect::Manual,
});
let headers = web_sys::Headers::new().map_err(js_error)?;
for (key, value) in &request.headers {
headers
.append(key, value)
.map_err(|e| Error::Header(js_message(&e)))?;
}
if let Some(cookie) = request.cookie_header() {
headers.append("cookie", &cookie).map_err(js_error)?;
}
if let Some(json) = &request.json {
if !request.has_content_type() {
headers
.set("content-type", "application/json")
.map_err(js_error)?;
}
init.set_body(&JsValue::from_str(json));
} else if let Some(form) = &request.form {
let body = form_urlencoded::Serializer::new(String::new())
.extend_pairs(form.iter().map(|(k, v)| (k.as_str(), v.as_str())))
.finish();
if !request.has_content_type() {
headers
.set("content-type", "application/x-www-form-urlencoded")
.map_err(js_error)?;
}
init.set_body(&JsValue::from_str(&body));
}
init.set_headers(headers.as_ref());
let js_request = web_sys::Request::new_with_str_and_init(&request.url, &init)
.map_err(|e| Error::Request(js_message(&e)))?;
let js_response: web_sys::Response = JsFuture::from(fetch_with_request(&js_request))
.await
.map_err(js_error)?
.dyn_into()
.map_err(|_| Error::Transport("fetch did not return a Response".into()))?;
let status = js_response.status();
let js_headers = js_response.headers();
let mut headers = Vec::new();
if let Ok(Some(entries)) = js_sys::try_iter(js_headers.as_ref()) {
for entry in entries.flatten() {
let pair = js_sys::Array::from(&entry);
if let (Some(name), Some(value)) = (pair.get(0).as_string(), pair.get(1).as_string())
&& !name.eq_ignore_ascii_case("set-cookie")
{
headers.push((name, value));
}
}
}
for cookie in get_set_cookie(&js_headers) {
headers.push(("set-cookie".to_owned(), cookie));
}
let buffer = JsFuture::from(js_response.array_buffer().map_err(js_error)?)
.await
.map_err(js_error)?;
let body = js_sys::Uint8Array::new(&buffer).to_vec();
Ok(Response::new(status, headers, body))
}
fn get_set_cookie(headers: &web_sys::Headers) -> Vec<String> {
js_sys::Reflect::get(headers.as_ref(), &JsValue::from_str("getSetCookie"))
.ok()
.and_then(|f| f.dyn_into::<js_sys::Function>().ok())
.and_then(|f| f.call0(headers.as_ref()).ok())
.map(|values| {
js_sys::Array::from(&values)
.iter()
.filter_map(|value| value.as_string())
.collect()
})
.unwrap_or_default()
}
fn js_message(value: &JsValue) -> String {
if let Some(error) = value.dyn_ref::<js_sys::Error>() {
return String::from(error.message());
}
value.as_string().unwrap_or_else(|| format!("{value:?}"))
}
fn js_error(value: JsValue) -> Error {
Error::Transport(js_message(&value))
}
#[cfg(test)]
mod tests {
use super::get_set_cookie;
use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test]
fn reads_every_set_cookie_value() {
let headers = web_sys::Headers::new().unwrap();
headers.append("set-cookie", "a=1; Path=/").unwrap();
headers.append("set-cookie", "b=2; HttpOnly").unwrap();
assert_eq!(
get_set_cookie(&headers),
vec!["a=1; Path=/", "b=2; HttpOnly"]
);
}
}