1use http::header::USER_AGENT;
2use http::{HeaderMap, HeaderValue, Method};
3use js_sys::Promise;
4use std::convert::TryInto;
5use std::{fmt, future::Future, sync::Arc};
6use url::Url;
7use wasm_bindgen::prelude::{wasm_bindgen, UnwrapThrowExt as _};
8use wasm_bindgen::JsCast;
9
10use super::{AbortGuard, Request, RequestBuilder, Response};
11use crate::IntoUrl;
12
13#[wasm_bindgen]
14extern "C" {
15 #[wasm_bindgen(js_name = fetch)]
16 fn fetch_with_request(input: &web_sys::Request) -> Promise;
17}
18
19fn js_fetch(req: &web_sys::Request) -> Promise {
20 use wasm_bindgen::{JsCast, JsValue};
21 let global = js_sys::global();
22
23 if let Ok(true) = js_sys::Reflect::has(&global, &JsValue::from_str("ServiceWorkerGlobalScope"))
24 {
25 global
26 .unchecked_into::<web_sys::ServiceWorkerGlobalScope>()
27 .fetch_with_request(req)
28 } else {
29 fetch_with_request(req)
31 }
32}
33
34#[derive(Clone)]
40pub struct Client {
41 config: Arc<Config>,
42}
43
44pub struct ClientBuilder {
46 config: Config,
47}
48
49impl Client {
50 pub fn new() -> Self {
52 Client::builder().build().unwrap_throw()
53 }
54
55 pub fn builder() -> ClientBuilder {
59 ClientBuilder::new()
60 }
61
62 pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
68 self.request(Method::GET, url)
69 }
70
71 pub fn post<U: IntoUrl>(&self, url: U) -> RequestBuilder {
77 self.request(Method::POST, url)
78 }
79
80 pub fn put<U: IntoUrl>(&self, url: U) -> RequestBuilder {
86 self.request(Method::PUT, url)
87 }
88
89 pub fn patch<U: IntoUrl>(&self, url: U) -> RequestBuilder {
95 self.request(Method::PATCH, url)
96 }
97
98 pub fn delete<U: IntoUrl>(&self, url: U) -> RequestBuilder {
104 self.request(Method::DELETE, url)
105 }
106
107 pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder {
113 self.request(Method::HEAD, url)
114 }
115
116 pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
125 let req = url.into_url().map(move |url| Request::new(method, url));
126 RequestBuilder::new(self.clone(), req)
127 }
128
129 pub fn execute(
142 &self,
143 request: Request,
144 ) -> impl Future<Output = Result<Response, crate::Error>> {
145 self.execute_request(request)
146 }
147
148 fn merge_headers(&self, req: &mut Request) {
150 use http::header::Entry;
151 let headers: &mut HeaderMap = req.headers_mut();
152 for (key, value) in self.config.headers.iter() {
155 if let Entry::Vacant(entry) = headers.entry(key) {
156 entry.insert(value.clone());
157 }
158 }
159 }
160
161 pub(super) fn execute_request(
162 &self,
163 mut req: Request,
164 ) -> impl Future<Output = crate::Result<Response>> {
165 self.merge_headers(&mut req);
166 fetch(req)
167 }
168}
169
170impl Default for Client {
171 fn default() -> Self {
172 Self::new()
173 }
174}
175
176impl fmt::Debug for Client {
177 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
178 let mut builder = f.debug_struct("Client");
179 self.config.fmt_fields(&mut builder);
180 builder.finish()
181 }
182}
183
184impl fmt::Debug for ClientBuilder {
185 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
186 let mut builder = f.debug_struct("ClientBuilder");
187 self.config.fmt_fields(&mut builder);
188 builder.finish()
189 }
190}
191
192#[allow(deprecated)]
196async fn fetch(req: Request) -> crate::Result<Response> {
197 let mut init = web_sys::RequestInit::new();
199 init.method(req.method().as_str());
200
201 if let Some(referrer) = &req.referrer {
202 init.set_referrer(referrer);
203 }
204
205 if let Some(policy) = &req.referrer_policy {
206 let policy = match policy.as_str() {
207 "no-referrer" => web_sys::ReferrerPolicy::NoReferrer,
208 "no-referrer-when-downgrade" => web_sys::ReferrerPolicy::NoReferrerWhenDowngrade,
209 "origin" => web_sys::ReferrerPolicy::Origin,
210 "origin-when-cross-origin" => web_sys::ReferrerPolicy::OriginWhenCrossOrigin,
211 "same-origin" => web_sys::ReferrerPolicy::SameOrigin,
212 "strict-origin" => web_sys::ReferrerPolicy::StrictOrigin,
213 "strict-origin-when-cross-origin" => {
214 web_sys::ReferrerPolicy::StrictOriginWhenCrossOrigin
215 }
216 "unsafe-url" => web_sys::ReferrerPolicy::UnsafeUrl,
217 _ => web_sys::ReferrerPolicy::None,
218 };
219 init.set_referrer_policy(policy);
220 }
221
222 let js_headers = web_sys::Headers::new()
224 .map_err(crate::error::wasm)
225 .map_err(crate::error::builder)?;
226
227 for (name, value) in req.headers() {
228 js_headers
229 .append(
230 name.as_str(),
231 value.to_str().map_err(crate::error::builder)?,
232 )
233 .map_err(crate::error::wasm)
234 .map_err(crate::error::builder)?;
235 }
236 init.headers(&js_headers.into());
237
238 if !req.cors {
240 init.mode(web_sys::RequestMode::NoCors);
241 }
242
243 if let Some(creds) = req.credentials {
244 init.credentials(creds);
245 }
246
247 if let Some(cache) = req.cache {
248 init.set_cache(cache);
249 }
250
251 if let Some(body) = req.body() {
252 if !body.is_empty() {
253 init.body(Some(body.to_js_value()?.as_ref()));
254 }
255 }
256
257 let mut abort = AbortGuard::new()?;
258 if let Some(timeout) = req.timeout() {
259 abort.timeout(*timeout);
260 }
261 init.signal(Some(&abort.signal()));
262
263 let js_req = web_sys::Request::new_with_str_and_init(req.url().as_str(), &init)
264 .map_err(crate::error::wasm)
265 .map_err(crate::error::builder)?;
266
267 let p = js_fetch(&js_req);
269 let js_resp = super::promise::<web_sys::Response>(p)
270 .await
271 .map_err(|error| {
272 if error.to_string() == "JsValue(\"reqwest::errors::TimedOut\")" {
273 crate::error::TimedOut.into()
274 } else {
275 error
276 }
277 })
278 .map_err(crate::error::request)?;
279
280 let mut resp = http::Response::builder().status(js_resp.status());
282
283 let url = Url::parse(&js_resp.url()).expect_throw("url parse");
284
285 let js_headers = js_resp.headers();
286 for item in js_headers.entries() {
287 let item = item.expect_throw("headers iterator doesn't throw");
288 let item: js_sys::Array = item.dyn_into().expect_throw("header item is an array");
289
290 let name = item
291 .get(0)
292 .as_string()
293 .expect_throw("header name is a string");
294
295 let value = item
296 .get(1)
297 .as_string()
298 .expect_throw("header value is a string");
299
300 resp = resp.header(&name, &value);
301 }
302
303 resp.body(js_resp)
304 .map(|resp| Response::new(resp, url, abort))
305 .map_err(crate::error::request)
306}
307
308impl ClientBuilder {
311 pub fn new() -> Self {
315 ClientBuilder {
316 config: Config::default(),
317 }
318 }
319
320 pub fn build(mut self) -> Result<Client, crate::Error> {
322 if let Some(err) = self.config.error {
323 return Err(err);
324 }
325
326 let config = std::mem::take(&mut self.config);
327 Ok(Client {
328 config: Arc::new(config),
329 })
330 }
331
332 pub fn user_agent<V>(mut self, value: V) -> ClientBuilder
334 where
335 V: TryInto<HeaderValue>,
336 V::Error: Into<http::Error>,
337 {
338 match value.try_into() {
339 Ok(value) => {
340 self.config.headers.insert(USER_AGENT, value);
341 }
342 Err(e) => {
343 self.config.error = Some(crate::error::builder(e.into()));
344 }
345 }
346 self
347 }
348
349 pub fn default_headers(mut self, headers: HeaderMap) -> ClientBuilder {
351 for (key, value) in headers.iter() {
352 self.config.headers.insert(key, value.clone());
353 }
354 self
355 }
356}
357
358impl Default for ClientBuilder {
359 fn default() -> Self {
360 Self::new()
361 }
362}
363
364#[derive(Debug)]
365struct Config {
366 headers: HeaderMap,
367 error: Option<crate::Error>,
368}
369
370impl Default for Config {
371 fn default() -> Config {
372 Config {
373 headers: HeaderMap::new(),
374 error: None,
375 }
376 }
377}
378
379impl Config {
380 fn fmt_fields(&self, f: &mut fmt::DebugStruct<'_, '_>) {
381 f.field("default_headers", &self.headers);
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use wasm_bindgen_test::*;
388
389 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
390
391 #[wasm_bindgen_test]
392 async fn default_headers() {
393 use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
394
395 let mut headers = HeaderMap::new();
396 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
397 headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
398 let client = crate::Client::builder()
399 .default_headers(headers)
400 .build()
401 .expect("client");
402 let mut req = client
403 .get("https://www.example.com")
404 .build()
405 .expect("request");
406 client.merge_headers(&mut req);
408
409 let test_headers = req.headers();
410 assert!(test_headers.get(CONTENT_TYPE).is_some(), "content-type");
411 assert!(test_headers.get("x-custom").is_some(), "custom header");
412 assert!(test_headers.get("accept").is_none(), "no accept header");
413 }
414
415 #[wasm_bindgen_test]
416 async fn default_headers_clone() {
417 use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
418
419 let mut headers = HeaderMap::new();
420 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
421 headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
422 let client = crate::Client::builder()
423 .default_headers(headers)
424 .build()
425 .expect("client");
426
427 let mut req = client
428 .get("https://www.example.com")
429 .header(CONTENT_TYPE, "text/plain")
430 .build()
431 .expect("request");
432 client.merge_headers(&mut req);
433 let headers1 = req.headers();
434
435 assert_eq!(
437 headers1.get(CONTENT_TYPE).unwrap(),
438 "text/plain",
439 "request headers override defaults"
440 );
441
442 let mut req2 = client
444 .get("https://www.example.com/x")
445 .build()
446 .expect("req 2");
447 client.merge_headers(&mut req2);
448 let headers2 = req2.headers();
449 assert_eq!(
450 headers2.get(CONTENT_TYPE).unwrap(),
451 "application/json",
452 "request headers don't change client defaults"
453 );
454 }
455
456 #[wasm_bindgen_test]
457 fn user_agent_header() {
458 use crate::header::USER_AGENT;
459
460 let client = crate::Client::builder()
461 .user_agent("FooBar/1.2.3")
462 .build()
463 .expect("client");
464
465 let mut req = client
466 .get("https://www.example.com")
467 .build()
468 .expect("request");
469
470 client.merge_headers(&mut req);
472 let headers1 = req.headers();
473
474 assert_eq!(
476 headers1.get(USER_AGENT).unwrap(),
477 "FooBar/1.2.3",
478 "The user-agent header was not set: {req:#?}"
479 );
480
481 let mut req2 = client
484 .get("https://www.example.com")
485 .header(USER_AGENT, "Another-User-Agent/42")
486 .build()
487 .expect("request 2");
488
489 client.merge_headers(&mut req2);
490 let headers2 = req2.headers();
491
492 assert_eq!(
493 headers2.get(USER_AGENT).expect("headers2 user agent"),
494 "Another-User-Agent/42",
495 "Was not able to overwrite the User-Agent value on the request-builder"
496 );
497 }
498}