use super::Task;
use crate::callback::Callback;
use crate::format::{Binary, Format, Text};
use serde::Serialize;
use std::collections::HashMap;
use std::fmt;
use stdweb::serde::Serde;
use stdweb::unstable::{TryFrom, TryInto};
use stdweb::web::ArrayBuffer;
use stdweb::{JsSerialize, Value};
#[allow(unused_imports)]
use stdweb::{_js_impl, js};
use thiserror::Error;
pub use http::{HeaderMap, Method, Request, Response, StatusCode, Uri};
#[derive(Serialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub enum Cache {
#[serde(rename = "default")]
DefaultCache,
NoStore,
Reload,
NoCache,
ForceCache,
OnlyIfCached,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub enum Credentials {
Omit,
Include,
SameOrigin,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub enum Mode {
SameOrigin,
NoCors,
Cors,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub enum Redirect {
Follow,
Error,
Manual,
}
#[derive(Debug)]
pub enum Referrer {
SameOriginUrl(String),
AboutClient,
Empty,
}
impl Serialize for Referrer {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match *self {
Referrer::SameOriginUrl(ref s) => serializer.serialize_str(s),
Referrer::AboutClient => {
serializer.serialize_unit_variant("Referrer", 0, "about:client")
}
Referrer::Empty => serializer.serialize_unit_variant("Referrer", 1, ""),
}
}
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub enum ReferrerPolicy {
NoReferrer,
NoReferrerWhenDowngrade,
SameOrigin,
Origin,
StrictOrigin,
OriginWhenCrossOrigin,
StrictOriginWhenCrossOrigin,
UnsafeUrl,
}
#[derive(Serialize, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FetchOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub cache: Option<Cache>,
#[serde(skip_serializing_if = "Option::is_none")]
pub credentials: Option<Credentials>,
#[serde(skip_serializing_if = "Option::is_none")]
pub redirect: Option<Redirect>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<Mode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub referrer: Option<Referrer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub referrer_policy: Option<ReferrerPolicy>,
#[serde(skip_serializing_if = "Option::is_none")]
pub integrity: Option<String>,
}
#[derive(Debug, Error)]
enum FetchError {
#[error("failed response")]
FailedResponse,
}
#[must_use]
pub struct FetchTask(Option<Value>);
impl fmt::Debug for FetchTask {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("FetchTask")
}
}
#[derive(Default, Debug)]
pub struct FetchService {}
impl FetchService {
pub fn new() -> Self {
Self {}
}
pub fn fetch<IN, OUT: 'static>(
&mut self,
request: Request<IN>,
callback: Callback<Response<OUT>>,
) -> FetchTask
where
IN: Into<Text>,
OUT: From<Text>,
{
fetch_impl::<IN, OUT, String, String>(false, request, None, callback)
}
pub fn fetch_with_options<IN, OUT: 'static>(
&mut self,
request: Request<IN>,
options: FetchOptions,
callback: Callback<Response<OUT>>,
) -> FetchTask
where
IN: Into<Text>,
OUT: From<Text>,
{
fetch_impl::<IN, OUT, String, String>(false, request, Some(options), callback)
}
pub fn fetch_binary<IN, OUT: 'static>(
&mut self,
request: Request<IN>,
callback: Callback<Response<OUT>>,
) -> FetchTask
where
IN: Into<Binary>,
OUT: From<Binary>,
{
fetch_impl::<IN, OUT, Vec<u8>, ArrayBuffer>(true, request, None, callback)
}
pub fn fetch_binary_with_options<IN, OUT: 'static>(
&mut self,
request: Request<IN>,
options: FetchOptions,
callback: Callback<Response<OUT>>,
) -> FetchTask
where
IN: Into<Binary>,
OUT: From<Binary>,
{
fetch_impl::<IN, OUT, Vec<u8>, ArrayBuffer>(true, request, Some(options), callback)
}
}
fn fetch_impl<IN, OUT: 'static, T, X>(
binary: bool,
request: Request<IN>,
options: Option<FetchOptions>,
callback: Callback<Response<OUT>>,
) -> FetchTask
where
IN: Into<Format<T>>,
OUT: From<Format<T>>,
T: JsSerialize,
X: TryFrom<Value> + Into<T>,
{
let (parts, body) = request.into_parts();
let header_map: HashMap<&str, &str> = parts
.headers
.iter()
.map(|(k, v)| {
(
k.as_str(),
v.to_str().unwrap_or_else(|_| {
panic!("Unparsable request header {}: {:?}", k.as_str(), v)
}),
)
})
.collect();
let uri = format!("{}", parts.uri);
let method = parts.method.as_str();
let body = body.into().ok();
let callback = move |success: bool, status: u16, headers: HashMap<String, String>, data: X| {
let mut response_builder = Response::builder();
if let Ok(status) = StatusCode::from_u16(status) {
response_builder = response_builder.status(status);
}
for (key, values) in headers {
response_builder = response_builder.header(key.as_str(), values.as_str());
}
let data = if success {
Ok(data.into())
} else {
Err(FetchError::FailedResponse.into())
};
let out = OUT::from(data);
let response = response_builder.body(out).unwrap();
callback.emit(response);
};
#[allow(clippy::too_many_arguments)]
let handle = js! {
var body = @{body};
if (@{binary} && body != null) {
body = Uint8Array.from(body);
}
var callback = @{callback};
var abortController = AbortController ? new AbortController() : null;
var handle = {
active: true,
callback,
abortController,
};
var init = {
method: @{method},
body: body,
headers: @{header_map},
};
var opts = @{Serde(options)} || {};
for (var attrname in opts) {
init[attrname] = opts[attrname];
}
if (abortController && !("signal" in init)) {
init.signal = abortController.signal;
}
fetch(@{uri}, init).then(function(response) {
var promise = (@{binary}) ? response.arrayBuffer() : response.text();
var status = response.status;
var headers = {};
response.headers.forEach(function(value, key) {
headers[key] = value;
});
promise.then(function(data) {
if (handle.active == true) {
handle.active = false;
callback(true, status, headers, data);
callback.drop();
}
}).catch(function(err) {
if (handle.active == true) {
handle.active = false;
callback(false, status, headers, data);
callback.drop();
}
});
}).catch(function(e) {
if (handle.active == true) {
var data = (@{binary}) ? new ArrayBuffer() : "";
handle.active = false;
callback(false, 408, {}, data);
callback.drop();
}
});
return handle;
};
FetchTask(Some(handle))
}
impl Task for FetchTask {
fn is_active(&self) -> bool {
if let Some(ref task) = self.0 {
let result = js! {
var the_task = @{task};
return the_task.active &&
(!the_task.abortController || !the_task.abortController.signal.aborted);
};
result.try_into().unwrap_or(false)
} else {
false
}
}
}
impl Drop for FetchTask {
fn drop(&mut self) {
if self.is_active() {
let handle = self.0.take().unwrap();
js! { @(no_return)
var handle = @{handle};
handle.active = false;
handle.callback.drop();
if (handle.abortController) {
handle.abortController.abort();
}
}
}
}
}
#[cfg(test)]
#[cfg(feature = "wasm_test")]
mod tests {
use super::*;
use crate::callback::test_util::CallbackFuture;
use crate::format::{Json, Nothing};
use serde::Deserialize;
use ssri::Integrity;
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
wasm_bindgen_test_configure!(run_in_browser);
#[derive(Deserialize, Debug)]
struct HttpBin {
headers: HashMap<String, String>,
origin: String,
url: String,
}
#[derive(Deserialize, Debug)]
struct HttpBinHeaders {
headers: HashMap<String, String>,
}
#[test]
async fn fetch_referrer_default() {
let request = Request::get("https://httpbin.org/get")
.body(Nothing)
.unwrap();
let options = FetchOptions::default();
let cb_future = CallbackFuture::<Response<Json<Result<HttpBin, anyhow::Error>>>>::default();
let callback: Callback<_> = cb_future.clone().into();
let _task = FetchService::new().fetch_with_options(request, options, callback);
let resp = cb_future.await;
assert_eq!(resp.status(), StatusCode::OK);
if let Json(Ok(http_bin)) = resp.body() {
assert!(http_bin.headers.get("Referer").is_some());
} else {
assert!(false, "unexpected resp: {:#?}", resp);
}
}
#[test]
async fn fetch_referrer_same_origin_url() {
let request = Request::get("https://httpbin.org/get")
.body(Nothing)
.unwrap();
let options = FetchOptions {
referrer: Some(Referrer::SameOriginUrl(String::from("same-origin"))),
..FetchOptions::default()
};
let cb_future = CallbackFuture::<Response<Json<Result<HttpBin, anyhow::Error>>>>::default();
let callback: Callback<_> = cb_future.clone().into();
let _task = FetchService::new().fetch_with_options(request, options, callback);
let resp = cb_future.await;
assert_eq!(resp.status(), StatusCode::OK);
if let Json(Ok(http_bin)) = resp.body() {
let referrer = http_bin.headers.get("Referer").expect("no referer set");
assert!(referrer.ends_with("/same-origin"));
} else {
assert!(false, "unexpected resp: {:#?}", resp);
}
}
#[test]
async fn fetch_referrer_about_client() {
let request = Request::get("https://httpbin.org/get")
.body(Nothing)
.unwrap();
let options = FetchOptions {
referrer: Some(Referrer::AboutClient),
..FetchOptions::default()
};
let cb_future = CallbackFuture::<Response<Json<Result<HttpBin, anyhow::Error>>>>::default();
let callback: Callback<_> = cb_future.clone().into();
let _task = FetchService::new().fetch_with_options(request, options, callback);
let resp = cb_future.await;
assert_eq!(resp.status(), StatusCode::OK);
if let Json(Ok(http_bin)) = resp.body() {
assert!(http_bin.headers.get("Referer").is_some());
} else {
assert!(false, "unexpected resp: {:#?}", resp);
}
}
#[test]
async fn fetch_referrer_empty() {
let request = Request::get("https://httpbin.org/get")
.body(Nothing)
.unwrap();
let options = FetchOptions {
referrer: Some(Referrer::Empty),
..FetchOptions::default()
};
let cb_future = CallbackFuture::<Response<Json<Result<HttpBin, anyhow::Error>>>>::default();
let callback: Callback<_> = cb_future.clone().into();
let _task = FetchService::new().fetch_with_options(request, options, callback);
let resp = cb_future.await;
assert_eq!(resp.status(), StatusCode::OK);
if let Json(Ok(http_bin)) = resp.body() {
assert!(http_bin.headers.get("Referer").is_none());
} else {
assert!(false, "unexpected resp: {:#?}", resp);
}
}
#[test]
async fn fetch_redirect_default() {
let request = Request::get("https://httpbin.org/relative-redirect/1")
.body(Nothing)
.unwrap();
let options = FetchOptions::default();
let cb_future = CallbackFuture::<Response<Json<Result<HttpBin, anyhow::Error>>>>::default();
let callback: Callback<_> = cb_future.clone().into();
let _task = FetchService::new().fetch_with_options(request, options, callback);
let resp = cb_future.await;
assert_eq!(resp.status(), StatusCode::OK);
if let Json(Ok(http_bin)) = resp.body() {
assert_eq!(http_bin.url, String::from("https://httpbin.org/get"));
} else {
assert!(false, "unexpected resp: {:#?}", resp);
}
}
#[test]
async fn fetch_redirect_follow() {
let request = Request::get("https://httpbin.org/relative-redirect/1")
.body(Nothing)
.unwrap();
let options = FetchOptions {
redirect: Some(Redirect::Follow),
..FetchOptions::default()
};
let cb_future = CallbackFuture::<Response<Json<Result<HttpBin, anyhow::Error>>>>::default();
let callback: Callback<_> = cb_future.clone().into();
let _task = FetchService::new().fetch_with_options(request, options, callback);
let resp = cb_future.await;
assert_eq!(resp.status(), StatusCode::OK);
if let Json(Ok(http_bin)) = resp.body() {
assert_eq!(http_bin.url, String::from("https://httpbin.org/get"));
} else {
assert!(false, "unexpected resp: {:#?}", resp);
}
}
#[test]
async fn fetch_redirect_error() {
let request = Request::get("https://httpbin.org/relative-redirect/1")
.body(Nothing)
.unwrap();
let options = FetchOptions {
redirect: Some(Redirect::Error),
..FetchOptions::default()
};
let cb_future = CallbackFuture::<Response<Result<String, anyhow::Error>>>::default();
let callback: Callback<_> = cb_future.clone().into();
let _task = FetchService::new().fetch_with_options(request, options, callback);
let resp = cb_future.await;
assert_eq!(resp.status(), StatusCode::REQUEST_TIMEOUT);
}
#[test]
async fn fetch_redirect_manual() {
let request = Request::get("https://httpbin.org/relative-redirect/1")
.body(Nothing)
.unwrap();
let options = FetchOptions {
redirect: Some(Redirect::Manual),
..FetchOptions::default()
};
let cb_future = CallbackFuture::<Response<Result<String, anyhow::Error>>>::default();
let callback: Callback<_> = cb_future.clone().into();
let _task = FetchService::new().fetch_with_options(request, options, callback);
let resp = cb_future.await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.body().as_ref().unwrap(), &String::from(""));
}
#[test]
async fn fetch_integrity() {
let resource = "Yew SRI Test";
let request = Request::get(format!(
"https://httpbin.org/base64/{}",
base64::encode_config(resource, base64::URL_SAFE)
))
.body(Nothing)
.unwrap();
let options = FetchOptions {
integrity: Some(Integrity::from(resource).to_string()),
..FetchOptions::default()
};
let cb_future = CallbackFuture::<Response<Result<String, anyhow::Error>>>::default();
let callback: Callback<_> = cb_future.clone().into();
let _task = FetchService::new().fetch_with_options(request, options, callback);
let resp = cb_future.await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.body().as_ref().unwrap(), resource);
}
#[test]
async fn fetch_integrity_fail() {
let resource = "Yew SRI Test";
let request = Request::get(format!(
"https://httpbin.org/base64/{}",
base64::encode_config(resource, base64::URL_SAFE)
))
.body(Nothing)
.unwrap();
let options = FetchOptions {
integrity: Some(Integrity::from("Yew SRI Test fail").to_string()),
..FetchOptions::default()
};
let cb_future = CallbackFuture::<Response<Result<String, anyhow::Error>>>::default();
let callback: Callback<_> = cb_future.clone().into();
let _task = FetchService::new().fetch_with_options(request, options, callback);
let resp = cb_future.await;
assert!(resp.body().is_err());
}
#[test]
async fn fetch_referrer_policy_no_referrer() {
let request = Request::get("https://httpbin.org/headers")
.body(Nothing)
.unwrap();
let options = FetchOptions {
referrer_policy: Some(ReferrerPolicy::NoReferrer),
..FetchOptions::default()
};
let cb_future =
CallbackFuture::<Response<Json<Result<HttpBinHeaders, anyhow::Error>>>>::default();
let callback: Callback<_> = cb_future.clone().into();
let _task = FetchService::new().fetch_with_options(request, options, callback);
let resp = cb_future.await;
assert_eq!(resp.status(), StatusCode::OK);
if let Json(Ok(httpbin_headers)) = resp.body() {
assert_eq!(httpbin_headers.headers.get("Referer"), None);
} else {
assert!(false, "unexpected resp: {:#?}", resp);
}
}
#[test]
async fn fetch_referrer_policy_origin() {
let request = Request::get("https://httpbin.org/headers")
.body(Nothing)
.unwrap();
let options = FetchOptions {
referrer_policy: Some(ReferrerPolicy::Origin),
..FetchOptions::default()
};
let cb_future =
CallbackFuture::<Response<Json<Result<HttpBinHeaders, anyhow::Error>>>>::default();
let callback: Callback<_> = cb_future.clone().into();
let _task = FetchService::new().fetch_with_options(request, options, callback);
let resp = cb_future.await;
assert_eq!(resp.status(), StatusCode::OK);
if let Json(Ok(httpbin_headers)) = resp.body() {
assert!(httpbin_headers
.headers
.get("Referer")
.unwrap()
.starts_with(&stdweb::web::window().location().unwrap().origin().unwrap()));
} else {
assert!(false, "unexpected resp: {:#?}", resp);
}
}
}