1 use http::{HeaderMap, Method};
2 use js_sys::{Promise, JSON};
3 use std::{fmt, future::Future, sync::Arc};
4 use url::Url;
5 use wasm_bindgen::prelude::{wasm_bindgen, UnwrapThrowExt as _};
6
7 use super::{Request, RequestBuilder, Response};
8 use crate::IntoUrl;
9
10 #[wasm_bindgen]
11 extern "C" {
12 #[wasm_bindgen(js_name = fetch)]
fetch_with_request(input: &web_sys::Request) -> Promise13 fn fetch_with_request(input: &web_sys::Request) -> Promise;
14 }
15
js_fetch(req: &web_sys::Request) -> Promise16 fn js_fetch(req: &web_sys::Request) -> Promise {
17 use wasm_bindgen::{JsCast, JsValue};
18 let global = js_sys::global();
19
20 if let Ok(true) = js_sys::Reflect::has(&global, &JsValue::from_str("ServiceWorkerGlobalScope"))
21 {
22 global
23 .unchecked_into::<web_sys::ServiceWorkerGlobalScope>()
24 .fetch_with_request(req)
25 } else {
26 // browser
27 fetch_with_request(req)
28 }
29 }
30
31 /// dox
32 #[derive(Clone)]
33 pub struct Client {
34 config: Arc<Config>,
35 }
36
37 /// dox
38 pub struct ClientBuilder {
39 config: Config,
40 }
41
42 impl Client {
43 /// dox
new() -> Self44 pub fn new() -> Self {
45 Client::builder().build().unwrap_throw()
46 }
47
48 /// dox
builder() -> ClientBuilder49 pub fn builder() -> ClientBuilder {
50 ClientBuilder::new()
51 }
52
53 /// Convenience method to make a `GET` request to a URL.
54 ///
55 /// # Errors
56 ///
57 /// This method fails whenever supplied `Url` cannot be parsed.
get<U: IntoUrl>(&self, url: U) -> RequestBuilder58 pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
59 self.request(Method::GET, url)
60 }
61
62 /// Convenience method to make a `POST` request to a URL.
63 ///
64 /// # Errors
65 ///
66 /// This method fails whenever supplied `Url` cannot be parsed.
post<U: IntoUrl>(&self, url: U) -> RequestBuilder67 pub fn post<U: IntoUrl>(&self, url: U) -> RequestBuilder {
68 self.request(Method::POST, url)
69 }
70
71 /// Convenience method to make a `PUT` request to a URL.
72 ///
73 /// # Errors
74 ///
75 /// This method fails whenever supplied `Url` cannot be parsed.
put<U: IntoUrl>(&self, url: U) -> RequestBuilder76 pub fn put<U: IntoUrl>(&self, url: U) -> RequestBuilder {
77 self.request(Method::PUT, url)
78 }
79
80 /// Convenience method to make a `PATCH` request to a URL.
81 ///
82 /// # Errors
83 ///
84 /// This method fails whenever supplied `Url` cannot be parsed.
patch<U: IntoUrl>(&self, url: U) -> RequestBuilder85 pub fn patch<U: IntoUrl>(&self, url: U) -> RequestBuilder {
86 self.request(Method::PATCH, url)
87 }
88
89 /// Convenience method to make a `DELETE` request to a URL.
90 ///
91 /// # Errors
92 ///
93 /// This method fails whenever supplied `Url` cannot be parsed.
delete<U: IntoUrl>(&self, url: U) -> RequestBuilder94 pub fn delete<U: IntoUrl>(&self, url: U) -> RequestBuilder {
95 self.request(Method::DELETE, url)
96 }
97
98 /// Convenience method to make a `HEAD` request to a URL.
99 ///
100 /// # Errors
101 ///
102 /// This method fails whenever supplied `Url` cannot be parsed.
head<U: IntoUrl>(&self, url: U) -> RequestBuilder103 pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder {
104 self.request(Method::HEAD, url)
105 }
106
107 /// Start building a `Request` with the `Method` and `Url`.
108 ///
109 /// Returns a `RequestBuilder`, which will allow setting headers and
110 /// request body before sending.
111 ///
112 /// # Errors
113 ///
114 /// This method fails whenever supplied `Url` cannot be parsed.
request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder115 pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
116 let req = url.into_url().map(move |url| Request::new(method, url));
117 RequestBuilder::new(self.clone(), req)
118 }
119
120 /// Executes a `Request`.
121 ///
122 /// A `Request` can be built manually with `Request::new()` or obtained
123 /// from a RequestBuilder with `RequestBuilder::build()`.
124 ///
125 /// You should prefer to use the `RequestBuilder` and
126 /// `RequestBuilder::send()`.
127 ///
128 /// # Errors
129 ///
130 /// This method fails if there was an error while sending request,
131 /// redirect loop was detected or redirect limit was exhausted.
execute( &self, request: Request, ) -> impl Future<Output = Result<Response, crate::Error>>132 pub fn execute(
133 &self,
134 request: Request,
135 ) -> impl Future<Output = Result<Response, crate::Error>> {
136 self.execute_request(request)
137 }
138
139 // merge request headers with Client default_headers, prior to external http fetch
merge_headers(&self, req: &mut Request)140 fn merge_headers(&self, req: &mut Request) {
141 use http::header::Entry;
142 let headers: &mut HeaderMap = req.headers_mut();
143 // insert default headers in the request headers
144 // without overwriting already appended headers.
145 for (key, value) in self.config.headers.iter() {
146 if let Entry::Vacant(entry) = headers.entry(key) {
147 entry.insert(value.clone());
148 }
149 }
150 }
151
execute_request( &self, mut req: Request, ) -> impl Future<Output = crate::Result<Response>>152 pub(super) fn execute_request(
153 &self,
154 mut req: Request,
155 ) -> impl Future<Output = crate::Result<Response>> {
156 self.merge_headers(&mut req);
157 fetch(req)
158 }
159 }
160
161 impl Default for Client {
default() -> Self162 fn default() -> Self {
163 Self::new()
164 }
165 }
166
167 impl fmt::Debug for Client {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result168 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
169 let mut builder = f.debug_struct("Client");
170 self.config.fmt_fields(&mut builder);
171 builder.finish()
172 }
173 }
174
175 impl fmt::Debug for ClientBuilder {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result176 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
177 let mut builder = f.debug_struct("ClientBuilder");
178 self.config.fmt_fields(&mut builder);
179 builder.finish()
180 }
181 }
182
fetch(req: Request) -> crate::Result<Response>183 async fn fetch(req: Request) -> crate::Result<Response> {
184 // Build the js Request
185 let mut init = web_sys::RequestInit::new();
186 init.method(req.method().as_str());
187
188 // convert HeaderMap to Headers
189 let js_headers = web_sys::Headers::new()
190 .map_err(crate::error::wasm)
191 .map_err(crate::error::builder)?;
192
193 for (name, value) in req.headers() {
194 js_headers
195 .append(
196 name.as_str(),
197 value.to_str().map_err(crate::error::builder)?,
198 )
199 .map_err(crate::error::wasm)
200 .map_err(crate::error::builder)?;
201 }
202 init.headers(&js_headers.into());
203
204 // When req.cors is true, do nothing because the default mode is 'cors'
205 if !req.cors {
206 init.mode(web_sys::RequestMode::NoCors);
207 }
208
209 if let Some(creds) = req.credentials {
210 init.credentials(creds);
211 }
212
213 if let Some(body) = req.body() {
214 if !body.is_empty() {
215 init.body(Some(body.to_js_value()?.as_ref()));
216 }
217 }
218
219 let js_req = web_sys::Request::new_with_str_and_init(req.url().as_str(), &init)
220 .map_err(crate::error::wasm)
221 .map_err(crate::error::builder)?;
222
223 // Await the fetch() promise
224 let p = js_fetch(&js_req);
225 let js_resp = super::promise::<web_sys::Response>(p)
226 .await
227 .map_err(crate::error::request)?;
228
229 // Convert from the js Response
230 let mut resp = http::Response::builder().status(js_resp.status());
231
232 let url = Url::parse(&js_resp.url()).expect_throw("url parse");
233
234 let js_headers = js_resp.headers();
235 let js_iter = js_sys::try_iter(&js_headers)
236 .expect_throw("headers try_iter")
237 .expect_throw("headers have an iterator");
238
239 for item in js_iter {
240 let item = item.expect_throw("headers iterator doesn't throw");
241 let serialized_headers: String = JSON::stringify(&item)
242 .expect_throw("serialized headers")
243 .into();
244 let [name, value]: [String; 2] = serde_json::from_str(&serialized_headers)
245 .expect_throw("deserializable serialized headers");
246 resp = resp.header(&name, &value);
247 }
248
249 resp.body(js_resp)
250 .map(|resp| Response::new(resp, url))
251 .map_err(crate::error::request)
252 }
253
254 // ===== impl ClientBuilder =====
255
256 impl ClientBuilder {
257 /// dox
new() -> Self258 pub fn new() -> Self {
259 ClientBuilder {
260 config: Config::default(),
261 }
262 }
263
264 /// Returns a 'Client' that uses this ClientBuilder configuration
build(mut self) -> Result<Client, crate::Error>265 pub fn build(mut self) -> Result<Client, crate::Error> {
266 let config = std::mem::take(&mut self.config);
267 Ok(Client {
268 config: Arc::new(config),
269 })
270 }
271
272 /// Sets the default headers for every request
default_headers(mut self, headers: HeaderMap) -> ClientBuilder273 pub fn default_headers(mut self, headers: HeaderMap) -> ClientBuilder {
274 for (key, value) in headers.iter() {
275 self.config.headers.insert(key, value.clone());
276 }
277 self
278 }
279 }
280
281 impl Default for ClientBuilder {
default() -> Self282 fn default() -> Self {
283 Self::new()
284 }
285 }
286
287 #[derive(Clone, Debug)]
288 struct Config {
289 headers: HeaderMap,
290 }
291
292 impl Default for Config {
default() -> Config293 fn default() -> Config {
294 Config {
295 headers: HeaderMap::new(),
296 }
297 }
298 }
299
300 impl Config {
fmt_fields(&self, f: &mut fmt::DebugStruct<'_, '_>)301 fn fmt_fields(&self, f: &mut fmt::DebugStruct<'_, '_>) {
302 f.field("default_headers", &self.headers);
303 }
304 }
305
306 #[cfg(test)]
307 mod tests {
308 use wasm_bindgen_test::*;
309
310 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
311
312 #[wasm_bindgen_test]
default_headers()313 async fn default_headers() {
314 use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
315
316 let mut headers = HeaderMap::new();
317 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
318 headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
319 let client = crate::Client::builder()
320 .default_headers(headers)
321 .build()
322 .expect("client");
323 let mut req = client
324 .get("https://www.example.com")
325 .build()
326 .expect("request");
327 // merge headers as if client were about to issue fetch
328 client.merge_headers(&mut req);
329
330 let test_headers = req.headers();
331 assert!(test_headers.get(CONTENT_TYPE).is_some(), "content-type");
332 assert!(test_headers.get("x-custom").is_some(), "custom header");
333 assert!(test_headers.get("accept").is_none(), "no accept header");
334 }
335
336 #[wasm_bindgen_test]
default_headers_clone()337 async fn default_headers_clone() {
338 use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
339
340 let mut headers = HeaderMap::new();
341 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
342 headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
343 let client = crate::Client::builder()
344 .default_headers(headers)
345 .build()
346 .expect("client");
347
348 let mut req = client
349 .get("https://www.example.com")
350 .header(CONTENT_TYPE, "text/plain")
351 .build()
352 .expect("request");
353 client.merge_headers(&mut req);
354 let headers1 = req.headers();
355
356 // confirm that request headers override defaults
357 assert_eq!(
358 headers1.get(CONTENT_TYPE).unwrap(),
359 "text/plain",
360 "request headers override defaults"
361 );
362
363 // confirm that request headers don't change client defaults
364 let mut req2 = client
365 .get("https://www.example.com/x")
366 .build()
367 .expect("req 2");
368 client.merge_headers(&mut req2);
369 let headers2 = req2.headers();
370 assert_eq!(
371 headers2.get(CONTENT_TYPE).unwrap(),
372 "application/json",
373 "request headers don't change client defaults"
374 );
375 }
376 }
377