reqwest_h3/wasm/
client.rs1use http::header::USER_AGENT;
2use http::{HeaderMap, HeaderValue, Method};
3use js_sys::{Promise, JSON};
4use std::convert::TryInto;
5use std::{fmt, future::Future, sync::Arc};
6use url::Url;
7use wasm_bindgen::prelude::{wasm_bindgen, UnwrapThrowExt as _};
8
9use super::{AbortGuard, Request, RequestBuilder, Response};
10use crate::IntoUrl;
11
12#[wasm_bindgen]
13extern "C" {
14 #[wasm_bindgen(js_name = fetch)]
15 fn fetch_with_request(input: &web_sys::Request) -> Promise;
16}
17
18fn js_fetch(req: &web_sys::Request) -> Promise {
19 use wasm_bindgen::{JsCast, JsValue};
20 let global = js_sys::global();
21
22 if let Ok(true) = js_sys::Reflect::has(&global, &JsValue::from_str("ServiceWorkerGlobalScope"))
23 {
24 global
25 .unchecked_into::<web_sys::ServiceWorkerGlobalScope>()
26 .fetch_with_request(req)
27 } else {
28 fetch_with_request(req)
30 }
31}
32
33#[derive(Clone)]
35pub struct Client {
36 config: Arc<Config>,
37}
38
39pub struct ClientBuilder {
41 config: Config,
42}
43
44impl Client {
45 pub fn new() -> Self {
47 Client::builder().build().unwrap_throw()
48 }
49
50 pub fn builder() -> ClientBuilder {
52 ClientBuilder::new()
53 }
54
55 pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
61 self.request(Method::GET, url)
62 }
63
64 pub fn post<U: IntoUrl>(&self, url: U) -> RequestBuilder {
70 self.request(Method::POST, url)
71 }
72
73 pub fn put<U: IntoUrl>(&self, url: U) -> RequestBuilder {
79 self.request(Method::PUT, url)
80 }
81
82 pub fn patch<U: IntoUrl>(&self, url: U) -> RequestBuilder {
88 self.request(Method::PATCH, url)
89 }
90
91 pub fn delete<U: IntoUrl>(&self, url: U) -> RequestBuilder {
97 self.request(Method::DELETE, url)
98 }
99
100 pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder {
106 self.request(Method::HEAD, url)
107 }
108
109 pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
118 let req = url.into_url().map(move |url| Request::new(method, url));
119 RequestBuilder::new(self.clone(), req)
120 }
121
122 pub fn execute(
135 &self,
136 request: Request,
137 ) -> impl Future<Output = Result<Response, crate::Error>> {
138 self.execute_request(request)
139 }
140
141 fn merge_headers(&self, req: &mut Request) {
143 use http::header::Entry;
144 let headers: &mut HeaderMap = req.headers_mut();
145 for (key, value) in self.config.headers.iter() {
148 if let Entry::Vacant(entry) = headers.entry(key) {
149 entry.insert(value.clone());
150 }
151 }
152 }
153
154 pub(super) fn execute_request(
155 &self,
156 mut req: Request,
157 ) -> impl Future<Output = crate::Result<Response>> {
158 self.merge_headers(&mut req);
159 fetch(req)
160 }
161}
162
163impl Default for Client {
164 fn default() -> Self {
165 Self::new()
166 }
167}
168
169impl fmt::Debug for Client {
170 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
171 let mut builder = f.debug_struct("Client");
172 self.config.fmt_fields(&mut builder);
173 builder.finish()
174 }
175}
176
177impl fmt::Debug for ClientBuilder {
178 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
179 let mut builder = f.debug_struct("ClientBuilder");
180 self.config.fmt_fields(&mut builder);
181 builder.finish()
182 }
183}
184
185#[allow(deprecated)]
189async fn fetch(req: Request) -> crate::Result<Response> {
190 let mut init = web_sys::RequestInit::new();
192 init.method(req.method().as_str());
193
194 let js_headers = web_sys::Headers::new()
196 .map_err(crate::error::wasm)
197 .map_err(crate::error::builder)?;
198
199 for (name, value) in req.headers() {
200 js_headers
201 .append(
202 name.as_str(),
203 value.to_str().map_err(crate::error::builder)?,
204 )
205 .map_err(crate::error::wasm)
206 .map_err(crate::error::builder)?;
207 }
208 init.headers(&js_headers.into());
209
210 if !req.cors {
212 init.mode(web_sys::RequestMode::NoCors);
213 }
214
215 if let Some(creds) = req.credentials {
216 init.credentials(creds);
217 }
218
219 if let Some(body) = req.body() {
220 if !body.is_empty() {
221 init.body(Some(body.to_js_value()?.as_ref()));
222 }
223 }
224
225 let mut abort = AbortGuard::new()?;
226 if let Some(timeout) = req.timeout() {
227 abort.timeout(*timeout);
228 }
229 init.signal(Some(&abort.signal()));
230
231 let js_req = web_sys::Request::new_with_str_and_init(req.url().as_str(), &init)
232 .map_err(crate::error::wasm)
233 .map_err(crate::error::builder)?;
234
235 let p = js_fetch(&js_req);
237 let js_resp = super::promise::<web_sys::Response>(p)
238 .await
239 .map_err(crate::error::request)?;
240
241 let mut resp = http::Response::builder().status(js_resp.status());
243
244 let url = Url::parse(&js_resp.url()).expect_throw("url parse");
245
246 let js_headers = js_resp.headers();
247 let js_iter = js_sys::try_iter(&js_headers)
248 .expect_throw("headers try_iter")
249 .expect_throw("headers have an iterator");
250
251 for item in js_iter {
252 let item = item.expect_throw("headers iterator doesn't throw");
253 let serialized_headers: String = JSON::stringify(&item)
254 .expect_throw("serialized headers")
255 .into();
256 let [name, value]: [String; 2] = serde_json::from_str(&serialized_headers)
257 .expect_throw("deserializable serialized headers");
258 resp = resp.header(&name, &value);
259 }
260
261 resp.body(js_resp)
262 .map(|resp| Response::new(resp, url, abort))
263 .map_err(crate::error::request)
264}
265
266impl ClientBuilder {
269 pub fn new() -> Self {
271 ClientBuilder {
272 config: Config::default(),
273 }
274 }
275
276 pub fn build(mut self) -> Result<Client, crate::Error> {
278 if let Some(err) = self.config.error {
279 return Err(err);
280 }
281
282 let config = std::mem::take(&mut self.config);
283 Ok(Client {
284 config: Arc::new(config),
285 })
286 }
287
288 pub fn user_agent<V>(mut self, value: V) -> ClientBuilder
290 where
291 V: TryInto<HeaderValue>,
292 V::Error: Into<http::Error>,
293 {
294 match value.try_into() {
295 Ok(value) => {
296 self.config.headers.insert(USER_AGENT, value);
297 }
298 Err(e) => {
299 self.config.error = Some(crate::error::builder(e.into()));
300 }
301 }
302 self
303 }
304
305 pub fn default_headers(mut self, headers: HeaderMap) -> ClientBuilder {
307 for (key, value) in headers.iter() {
308 self.config.headers.insert(key, value.clone());
309 }
310 self
311 }
312}
313
314impl Default for ClientBuilder {
315 fn default() -> Self {
316 Self::new()
317 }
318}
319
320#[derive(Debug)]
321struct Config {
322 headers: HeaderMap,
323 error: Option<crate::Error>,
324}
325
326impl Default for Config {
327 fn default() -> Config {
328 Config {
329 headers: HeaderMap::new(),
330 error: None,
331 }
332 }
333}
334
335impl Config {
336 fn fmt_fields(&self, f: &mut fmt::DebugStruct<'_, '_>) {
337 f.field("default_headers", &self.headers);
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use wasm_bindgen_test::*;
344
345 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
346
347 #[wasm_bindgen_test]
348 async fn default_headers() {
349 use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
350
351 let mut headers = HeaderMap::new();
352 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
353 headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
354 let client = crate::Client::builder()
355 .default_headers(headers)
356 .build()
357 .expect("client");
358 let mut req = client
359 .get("https://www.example.com")
360 .build()
361 .expect("request");
362 client.merge_headers(&mut req);
364
365 let test_headers = req.headers();
366 assert!(test_headers.get(CONTENT_TYPE).is_some(), "content-type");
367 assert!(test_headers.get("x-custom").is_some(), "custom header");
368 assert!(test_headers.get("accept").is_none(), "no accept header");
369 }
370
371 #[wasm_bindgen_test]
372 async fn default_headers_clone() {
373 use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
374
375 let mut headers = HeaderMap::new();
376 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
377 headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
378 let client = crate::Client::builder()
379 .default_headers(headers)
380 .build()
381 .expect("client");
382
383 let mut req = client
384 .get("https://www.example.com")
385 .header(CONTENT_TYPE, "text/plain")
386 .build()
387 .expect("request");
388 client.merge_headers(&mut req);
389 let headers1 = req.headers();
390
391 assert_eq!(
393 headers1.get(CONTENT_TYPE).unwrap(),
394 "text/plain",
395 "request headers override defaults"
396 );
397
398 let mut req2 = client
400 .get("https://www.example.com/x")
401 .build()
402 .expect("req 2");
403 client.merge_headers(&mut req2);
404 let headers2 = req2.headers();
405 assert_eq!(
406 headers2.get(CONTENT_TYPE).unwrap(),
407 "application/json",
408 "request headers don't change client defaults"
409 );
410 }
411
412 #[wasm_bindgen_test]
413 fn user_agent_header() {
414 use crate::header::USER_AGENT;
415
416 let client = crate::Client::builder()
417 .user_agent("FooBar/1.2.3")
418 .build()
419 .expect("client");
420
421 let mut req = client
422 .get("https://www.example.com")
423 .build()
424 .expect("request");
425
426 client.merge_headers(&mut req);
428 let headers1 = req.headers();
429
430 assert_eq!(
432 headers1.get(USER_AGENT).unwrap(),
433 "FooBar/1.2.3",
434 "The user-agent header was not set: {req:#?}"
435 );
436
437 let mut req2 = client
440 .get("https://www.example.com")
441 .header(USER_AGENT, "Another-User-Agent/42")
442 .build()
443 .expect("request 2");
444
445 client.merge_headers(&mut req2);
446 let headers2 = req2.headers();
447
448 assert_eq!(
449 headers2.get(USER_AGENT).expect("headers2 user agent"),
450 "Another-User-Agent/42",
451 "Was not able to overwrite the User-Agent value on the request-builder"
452 );
453 }
454}