From 4fe07d81cfc51e6ab720ed5826b88687e762f002 Mon Sep 17 00:00:00 2001 From: stevelr <15311467+stevelr@users.noreply.github.com> Date: Mon, 16 Nov 2020 21:09:47 +0000 Subject: [PATCH] add ClientBuilder.default_headers() for wasm32 target (#1084) --- Cargo.toml | 9 +++ examples/form.rs | 15 +++- examples/simple.rs | 8 ++ src/wasm/client.rs | 162 ++++++++++++++++++++++++++++++++++++---- tests/badssl.rs | 2 + tests/client.rs | 1 + tests/multipart.rs | 1 + tests/proxy.rs | 1 + tests/redirect.rs | 1 + tests/support/server.rs | 1 + tests/timeouts.rs | 1 + tests/wasm_simple.rs | 24 ++++++ 12 files changed, 210 insertions(+), 16 deletions(-) create mode 100644 tests/wasm_simple.rs diff --git a/Cargo.toml b/Cargo.toml index cbed2f1..c094d5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,6 +132,7 @@ winreg = "0.7" js-sys = "0.3.45" wasm-bindgen = { version = "0.2.68", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4.18" +wasm-bindgen-test = "0.3" [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] version = "0.3.25" @@ -168,6 +169,14 @@ name = "tor_socks" path = "examples/tor_socks.rs" required-features = ["socks"] +[[example]] +name = "form" +path = "examples/form.rs" + +[[example]] +name = "simple" +path = "examples/simple.rs" + [[test]] name = "blocking" path = "tests/blocking.rs" diff --git a/examples/form.rs b/examples/form.rs index cfdb6e4..067f370 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -1,9 +1,20 @@ +// Short example of a POST request with form data. +// +#[cfg(not(target_arch = "wasm32"))] #[tokio::main] async fn main() { - reqwest::Client::new() + let response = reqwest::Client::new() .post("http://www.baidu.com") .form(&[("one", "1")]) .send() .await - .unwrap(); + .expect("send"); + println!("Response status {}", response.status()); } + +// The [cfg(not(target_arch = "wasm32"))] above prevent building the tokio::main function +// for wasm32 target, because tokio isn't compatible with wasm32. +// If you aren't building for wasm32, you don't need that line. +// The two lines below avoid the "'main' function not found" error when building for wasm32 target. +#[cfg(target_arch = "wasm32")] +fn main() {} diff --git a/examples/simple.rs b/examples/simple.rs index b5a9adc..cbf8d83 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -3,6 +3,7 @@ // This is using the `tokio` runtime. You'll need the following dependency: // // `tokio = { version = "0.2", features = ["macros"] }` +#[cfg(not(target_arch = "wasm32"))] #[tokio::main] async fn main() -> Result<(), reqwest::Error> { let res = reqwest::get("https://hyper.rs").await?; @@ -15,3 +16,10 @@ async fn main() -> Result<(), reqwest::Error> { Ok(()) } + +// The [cfg(not(target_arch = "wasm32"))] above prevent building the tokio::main function +// for wasm32 target, because tokio isn't compatible with wasm32. +// If you aren't building for wasm32, you don't need that line. +// The two lines below avoid the "'main' function not found" error when building for wasm32 target. +#[cfg(target_arch = "wasm32")] +fn main() {} diff --git a/src/wasm/client.rs b/src/wasm/client.rs index 2dca78b..d35891c 100644 --- a/src/wasm/client.rs +++ b/src/wasm/client.rs @@ -1,8 +1,8 @@ -use http::Method; -use std::future::Future; -use wasm_bindgen::prelude::{wasm_bindgen, UnwrapThrowExt as _}; +use http::{HeaderMap, Method}; use js_sys::Promise; +use std::{fmt, future::Future, sync::Arc}; use url::Url; +use wasm_bindgen::prelude::{wasm_bindgen, UnwrapThrowExt as _}; use super::{Request, RequestBuilder, Response}; use crate::IntoUrl; @@ -30,12 +30,15 @@ fn js_fetch(req: &web_sys::Request) -> Promise { } /// dox -#[derive(Clone, Debug)] -pub struct Client(()); +#[derive(Clone)] +pub struct Client { + config: Arc, +} /// dox -#[derive(Debug)] -pub struct ClientBuilder(()); +pub struct ClientBuilder { + config: Config, +} impl Client { /// dox @@ -134,10 +137,24 @@ impl Client { self.execute_request(request) } + // merge request headers with Client default_headers, prior to external http fetch + fn merge_headers(&self, req: &mut Request) { + use http::header::Entry; + let headers: &mut HeaderMap = req.headers_mut(); + // insert default headers in the request headers + // without overwriting already appended headers. + for (key, value) in self.config.headers.iter() { + if let Entry::Vacant(entry) = headers.entry(key) { + entry.insert(value.clone()); + } + } + } + pub(super) fn execute_request( &self, - req: Request, + mut req: Request, ) -> impl Future> { + self.merge_headers(&mut req); fetch(req) } } @@ -148,11 +165,28 @@ impl Default for Client { } } +impl fmt::Debug for Client { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut builder = f.debug_struct("Client"); + self.config.fmt_fields(&mut builder); + builder.finish() + } +} + +impl fmt::Debug for ClientBuilder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut builder = f.debug_struct("ClientBuilder"); + self.config.fmt_fields(&mut builder); + builder.finish() + } +} + async fn fetch(req: Request) -> crate::Result { // Build the js Request let mut init = web_sys::RequestInit::new(); init.method(req.method().as_str()); + // convert HeaderMap to Headers let js_headers = web_sys::Headers::new() .map_err(crate::error::wasm) .map_err(crate::error::builder)?; @@ -190,8 +224,7 @@ async fn fetch(req: Request) -> crate::Result { .map_err(crate::error::request)?; // Convert from the js Response - let mut resp = http::Response::builder() - .status(js_resp.status()); + let mut resp = http::Response::builder().status(js_resp.status()); let url = Url::parse(&js_resp.url()).expect_throw("url parse"); @@ -219,12 +252,25 @@ async fn fetch(req: Request) -> crate::Result { impl ClientBuilder { /// dox pub fn new() -> Self { - ClientBuilder(()) + ClientBuilder { + config: Config::default(), + } } - /// dox - pub fn build(self) -> Result { - Ok(Client(())) + /// Returns a 'Client' that uses this ClientBuilder configuration + pub fn build(mut self) -> Result { + let config = std::mem::take(&mut self.config); + Ok(Client { + config: Arc::new(config), + }) + } + + /// Sets the default headers for every request + pub fn default_headers(mut self, headers: HeaderMap) -> ClientBuilder { + for (key, value) in headers.iter() { + self.config.headers.insert(key, value.clone()); + } + self } } @@ -233,3 +279,91 @@ impl Default for ClientBuilder { Self::new() } } + +#[derive(Clone, Debug)] +struct Config { + headers: HeaderMap, +} + +impl Default for Config { + fn default() -> Config { + Config { + headers: HeaderMap::new(), + } + } +} + +impl Config { + fn fmt_fields(&self, f: &mut fmt::DebugStruct<'_, '_>) { + f.field("default_headers", &self.headers); + } +} + +use wasm_bindgen_test::*; + +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +async fn default_headers() { + use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; + + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet")); + let client = crate::Client::builder() + .default_headers(headers) + .build() + .expect("client"); + let mut req = client + .get("https://www.example.com") + .build() + .expect("request"); + // merge headers as if client were about to issue fetch + client.merge_headers(&mut req); + + let test_headers = req.headers(); + assert!(test_headers.get(CONTENT_TYPE).is_some(), "content-type"); + assert!(test_headers.get("x-custom").is_some(), "custom header"); + assert!(test_headers.get("accept").is_none(), "no accept header"); +} + +#[wasm_bindgen_test] +async fn default_headers_clone() { + use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; + + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet")); + let client = crate::Client::builder() + .default_headers(headers) + .build() + .expect("client"); + + let mut req = client + .get("https://www.example.com") + .header(CONTENT_TYPE, "text/plain") + .build() + .expect("request"); + client.merge_headers(&mut req); + let headers1 = req.headers(); + + // confirm that request headers override defaults + assert_eq!( + headers1.get(CONTENT_TYPE).unwrap(), + "text/plain", + "request headers override defaults" + ); + + // confirm that request headers don't change client defaults + let mut req2 = client + .get("https://www.example.com/x") + .build() + .expect("req 2"); + client.merge_headers(&mut req2); + let headers2 = req2.headers(); + assert_eq!( + headers2.get(CONTENT_TYPE).unwrap(), + "application/json", + "request headers don't change client defaults" + ); +} diff --git a/tests/badssl.rs b/tests/badssl.rs index bb3f737..ac3435c 100644 --- a/tests/badssl.rs +++ b/tests/badssl.rs @@ -1,3 +1,5 @@ +#![cfg(not(target_arch = "wasm32"))] + #[cfg(feature = "__tls")] #[tokio::test] async fn test_badssl_modern() { diff --git a/tests/client.rs b/tests/client.rs index cde828b..75b801f 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -1,3 +1,4 @@ +#![cfg(not(target_arch = "wasm32"))] mod support; use futures_util::stream::StreamExt; use support::*; diff --git a/tests/multipart.rs b/tests/multipart.rs index 38896ed..0d60055 100644 --- a/tests/multipart.rs +++ b/tests/multipart.rs @@ -1,3 +1,4 @@ +#![cfg(not(target_arch = "wasm32"))] mod support; use futures_util::stream::StreamExt; use support::*; diff --git a/tests/proxy.rs b/tests/proxy.rs index 11cab33..1a358c7 100644 --- a/tests/proxy.rs +++ b/tests/proxy.rs @@ -1,3 +1,4 @@ +#![cfg(not(target_arch = "wasm32"))] mod support; use support::*; diff --git a/tests/redirect.rs b/tests/redirect.rs index b2e4534..c1621ca 100644 --- a/tests/redirect.rs +++ b/tests/redirect.rs @@ -1,3 +1,4 @@ +#![cfg(not(target_arch = "wasm32"))] mod support; use futures_util::stream::StreamExt; use support::*; diff --git a/tests/support/server.rs b/tests/support/server.rs index c00ca18..e645904 100644 --- a/tests/support/server.rs +++ b/tests/support/server.rs @@ -1,3 +1,4 @@ +#![cfg(not(target_arch = "wasm32"))] use std::convert::Infallible; use std::future::Future; use std::net; diff --git a/tests/timeouts.rs b/tests/timeouts.rs index 8c3a269..25c9924 100644 --- a/tests/timeouts.rs +++ b/tests/timeouts.rs @@ -1,3 +1,4 @@ +#![cfg(not(target_arch = "wasm32"))] mod support; use support::*; diff --git a/tests/wasm_simple.rs b/tests/wasm_simple.rs new file mode 100644 index 0000000..6062c57 --- /dev/null +++ b/tests/wasm_simple.rs @@ -0,0 +1,24 @@ +#![cfg(target_arch = "wasm32")] + +use wasm_bindgen::prelude::*; +use wasm_bindgen_test::*; +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen] +extern "C" { + // Use `js_namespace` here to bind `console.log(..)` instead of just + // `log(..)` + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); +} + +#[wasm_bindgen_test] +async fn simple_example() { + let res = reqwest::get("https://hyper.rs") + .await + .expect("http get example"); + log(&format!("Status: {}", res.status())); + + let body = res.text().await.expect("response to utf-8 text"); + log(&format!("Body:\n\n{}", body)); +}