rikka 0.1.0

Async HTTP client for native & WASM.
Documentation
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(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"]
        );
    }
}