reqwest_wasm/wasm/
client.rs1use http::{HeaderMap, Method};
2use js_sys::{Promise, JSON};
3use std::{fmt, future::Future, sync::Arc};
4use url::Url;
5use wasm_bindgen::prelude::{wasm_bindgen, UnwrapThrowExt as _};
6
7use super::{Request, RequestBuilder, Response};
8use crate::IntoUrl;
9
10#[wasm_bindgen]
11extern "C" {
12 #[wasm_bindgen(js_name = fetch)]
13 fn fetch_with_request(input: &web_sys::Request) -> Promise;
14}
15
16fn 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 fetch_with_request(req)
28 }
29}
30
31#[derive(Clone)]
33pub struct Client {
34 config: Arc<Config>,
35}
36
37pub struct ClientBuilder {
39 config: Config,
40}
41
42impl Client {
43 pub fn new() -> Self {
45 Client::builder().build().unwrap_throw()
46 }
47
48 pub fn builder() -> ClientBuilder {
50 ClientBuilder::new()
51 }
52
53 pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
59 self.request(Method::GET, url)
60 }
61
62 pub fn post<U: IntoUrl>(&self, url: U) -> RequestBuilder {
68 self.request(Method::POST, url)
69 }
70
71 pub fn put<U: IntoUrl>(&self, url: U) -> RequestBuilder {
77 self.request(Method::PUT, url)
78 }
79
80 pub fn patch<U: IntoUrl>(&self, url: U) -> RequestBuilder {
86 self.request(Method::PATCH, url)
87 }
88
89 pub fn delete<U: IntoUrl>(&self, url: U) -> RequestBuilder {
95 self.request(Method::DELETE, url)
96 }
97
98 pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder {
104 self.request(Method::HEAD, url)
105 }
106
107 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 pub fn execute(
133 &self,
134 request: Request,
135 ) -> impl Future<Output = Result<Response, crate::Error>> {
136 self.execute_request(request)
137 }
138
139 fn merge_headers(&self, req: &mut Request) {
141 use http::header::Entry;
142 let headers: &mut HeaderMap = req.headers_mut();
143 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
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
161impl Default for Client {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167impl fmt::Debug for Client {
168 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
175impl fmt::Debug for ClientBuilder {
176 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
183async fn fetch(req: Request) -> crate::Result<Response> {
184 let mut init = web_sys::RequestInit::new();
186 init.method(req.method().as_str());
187
188 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 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 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 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
254impl ClientBuilder {
257 pub fn new() -> Self {
259 ClientBuilder {
260 config: Config::default(),
261 }
262 }
263
264 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 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
281impl Default for ClientBuilder {
282 fn default() -> Self {
283 Self::new()
284 }
285}
286
287#[derive(Clone, Debug)]
288struct Config {
289 headers: HeaderMap,
290}
291
292impl Default for Config {
293 fn default() -> Config {
294 Config {
295 headers: HeaderMap::new(),
296 }
297 }
298}
299
300impl Config {
301 fn fmt_fields(&self, f: &mut fmt::DebugStruct<'_, '_>) {
302 f.field("default_headers", &self.headers);
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use wasm_bindgen_test::*;
309
310 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
311
312 #[wasm_bindgen_test]
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 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]
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 assert_eq!(
358 headers1.get(CONTENT_TYPE).unwrap(),
359 "text/plain",
360 "request headers override defaults"
361 );
362
363 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}