zed_reqwest/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(|error| {
240 if error.to_string() == "JsValue(\"reqwest::errors::TimedOut\")" {
241 crate::error::TimedOut.into()
242 } else {
243 error
244 }
245 })
246 .map_err(crate::error::request)?;
247
248 let mut resp = http::Response::builder().status(js_resp.status());
250
251 let url = Url::parse(&js_resp.url()).expect_throw("url parse");
252
253 let js_headers = js_resp.headers();
254 let js_iter = js_sys::try_iter(&js_headers)
255 .expect_throw("headers try_iter")
256 .expect_throw("headers have an iterator");
257
258 for item in js_iter {
259 let item = item.expect_throw("headers iterator doesn't throw");
260 let serialized_headers: String = JSON::stringify(&item)
261 .expect_throw("serialized headers")
262 .into();
263 let [name, value]: [String; 2] = serde_json::from_str(&serialized_headers)
264 .expect_throw("deserializable serialized headers");
265 resp = resp.header(&name, &value);
266 }
267
268 resp.body(js_resp)
269 .map(|resp| Response::new(resp, url, abort))
270 .map_err(crate::error::request)
271}
272
273impl ClientBuilder {
276 pub fn new() -> Self {
278 ClientBuilder {
279 config: Config::default(),
280 }
281 }
282
283 pub fn build(mut self) -> Result<Client, crate::Error> {
285 if let Some(err) = self.config.error {
286 return Err(err);
287 }
288
289 let config = std::mem::take(&mut self.config);
290 Ok(Client {
291 config: Arc::new(config),
292 })
293 }
294
295 pub fn user_agent<V>(mut self, value: V) -> ClientBuilder
297 where
298 V: TryInto<HeaderValue>,
299 V::Error: Into<http::Error>,
300 {
301 match value.try_into() {
302 Ok(value) => {
303 self.config.headers.insert(USER_AGENT, value);
304 }
305 Err(e) => {
306 self.config.error = Some(crate::error::builder(e.into()));
307 }
308 }
309 self
310 }
311
312 pub fn default_headers(mut self, headers: HeaderMap) -> ClientBuilder {
314 for (key, value) in headers.iter() {
315 self.config.headers.insert(key, value.clone());
316 }
317 self
318 }
319}
320
321impl Default for ClientBuilder {
322 fn default() -> Self {
323 Self::new()
324 }
325}
326
327#[derive(Debug)]
328struct Config {
329 headers: HeaderMap,
330 error: Option<crate::Error>,
331}
332
333impl Default for Config {
334 fn default() -> Config {
335 Config {
336 headers: HeaderMap::new(),
337 error: None,
338 }
339 }
340}
341
342impl Config {
343 fn fmt_fields(&self, f: &mut fmt::DebugStruct<'_, '_>) {
344 f.field("default_headers", &self.headers);
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use wasm_bindgen_test::*;
351
352 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
353
354 #[wasm_bindgen_test]
355 async fn default_headers() {
356 use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
357
358 let mut headers = HeaderMap::new();
359 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
360 headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
361 let client = crate::Client::builder()
362 .default_headers(headers)
363 .build()
364 .expect("client");
365 let mut req = client
366 .get("https://www.example.com")
367 .build()
368 .expect("request");
369 client.merge_headers(&mut req);
371
372 let test_headers = req.headers();
373 assert!(test_headers.get(CONTENT_TYPE).is_some(), "content-type");
374 assert!(test_headers.get("x-custom").is_some(), "custom header");
375 assert!(test_headers.get("accept").is_none(), "no accept header");
376 }
377
378 #[wasm_bindgen_test]
379 async fn default_headers_clone() {
380 use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
381
382 let mut headers = HeaderMap::new();
383 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
384 headers.insert("x-custom", HeaderValue::from_static("flibbertigibbet"));
385 let client = crate::Client::builder()
386 .default_headers(headers)
387 .build()
388 .expect("client");
389
390 let mut req = client
391 .get("https://www.example.com")
392 .header(CONTENT_TYPE, "text/plain")
393 .build()
394 .expect("request");
395 client.merge_headers(&mut req);
396 let headers1 = req.headers();
397
398 assert_eq!(
400 headers1.get(CONTENT_TYPE).unwrap(),
401 "text/plain",
402 "request headers override defaults"
403 );
404
405 let mut req2 = client
407 .get("https://www.example.com/x")
408 .build()
409 .expect("req 2");
410 client.merge_headers(&mut req2);
411 let headers2 = req2.headers();
412 assert_eq!(
413 headers2.get(CONTENT_TYPE).unwrap(),
414 "application/json",
415 "request headers don't change client defaults"
416 );
417 }
418
419 #[wasm_bindgen_test]
420 fn user_agent_header() {
421 use crate::header::USER_AGENT;
422
423 let client = crate::Client::builder()
424 .user_agent("FooBar/1.2.3")
425 .build()
426 .expect("client");
427
428 let mut req = client
429 .get("https://www.example.com")
430 .build()
431 .expect("request");
432
433 client.merge_headers(&mut req);
435 let headers1 = req.headers();
436
437 assert_eq!(
439 headers1.get(USER_AGENT).unwrap(),
440 "FooBar/1.2.3",
441 "The user-agent header was not set: {req:#?}"
442 );
443
444 let mut req2 = client
447 .get("https://www.example.com")
448 .header(USER_AGENT, "Another-User-Agent/42")
449 .build()
450 .expect("request 2");
451
452 client.merge_headers(&mut req2);
453 let headers2 = req2.headers();
454
455 assert_eq!(
456 headers2.get(USER_AGENT).expect("headers2 user agent"),
457 "Another-User-Agent/42",
458 "Was not able to overwrite the User-Agent value on the request-builder"
459 );
460 }
461}