Skip to main content

gloo_net/http/
request.rs

1use crate::http::{Headers, QueryParams, Response};
2use crate::{js_to_error, Error};
3use http::Method;
4use js_sys::{ArrayBuffer, Uint8Array};
5use std::convert::{From, TryFrom, TryInto};
6use std::fmt;
7use std::str::FromStr;
8use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue};
9use wasm_bindgen_futures::JsFuture;
10use web_sys::{
11    AbortSignal, FormData, ObserverCallback, ReadableStream, ReferrerPolicy, RequestCache,
12    RequestCredentials, RequestMode, RequestRedirect,
13};
14
15#[cfg(feature = "json")]
16#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
17use serde::de::DeserializeOwned;
18
19#[wasm_bindgen]
20extern "C" {
21    // Create a separate binding for `fetch` as a global, rather than using the
22    // existing Window/WorkerGlobalScope bindings defined by web_sys, for
23    // greater efficiency.
24    //
25    // https://github.com/rustwasm/wasm-bindgen/discussions/3863
26    #[wasm_bindgen(js_name = "fetch")]
27    fn fetch_with_request(request: &web_sys::Request) -> js_sys::Promise;
28}
29
30/// A wrapper round `web_sys::Request`: an http request to be used with the `fetch` API.
31pub struct RequestBuilder {
32    options: web_sys::RequestInit,
33    headers: Headers,
34    query: QueryParams,
35    url: String,
36}
37
38impl RequestBuilder {
39    /// Creates a new request that will be sent to `url`.
40    ///
41    /// Uses `GET` by default. `url` can be a `String`, a `&str`, or a `Cow<'a, str>`.
42    pub fn new(url: &str) -> Self {
43        Self {
44            options: web_sys::RequestInit::new(),
45            headers: Headers::new(),
46            query: QueryParams::new(),
47            url: url.into(),
48        }
49    }
50
51    /// Set the body for this request.
52    pub fn body(self, body: impl Into<JsValue>) -> Result<Request, Error> {
53        self.options.set_body(&body.into());
54
55        self.try_into()
56    }
57
58    /// A string indicating how the request will interact with the browser’s HTTP cache.
59    pub fn cache(self, cache: RequestCache) -> Self {
60        self.options.set_cache(cache);
61        self
62    }
63
64    /// Controls what browsers do with credentials (cookies, HTTP authentication entries, and TLS
65    /// client certificates).
66    pub fn credentials(self, credentials: RequestCredentials) -> Self {
67        self.options.set_credentials(credentials);
68        self
69    }
70
71    /// Replace _all_ the headers.
72    pub fn headers(mut self, headers: Headers) -> Self {
73        self.headers = headers;
74        self
75    }
76
77    /// Sets a header.
78    pub fn header(self, key: &str, value: &str) -> Self {
79        self.headers.set(key, value);
80        self
81    }
82
83    /// Append query parameters to the url, given as `(name, value)` tuples. Values can be of any
84    /// type that implements [`ToString`].
85    ///
86    /// It is possible to append the same parameters with the same name multiple times, so
87    /// `.query([("a", "1"), ("a", "2")])` results in the query string `a=1&a=2`.
88    ///
89    /// # Examples
90    ///
91    /// The query parameters can be passed in various different forms:
92    ///
93    /// ```
94    /// # fn no_run() {
95    /// use std::collections::HashMap;
96    /// use gloo_net::http::Request;
97    ///
98    /// let slice_params = [("key", "value")];
99    /// let vec_params = vec![("a", "3"), ("b", "4")];
100    /// let mut map_params: HashMap<&'static str, &'static str> = HashMap::new();
101    /// map_params.insert("key", "another_value");
102    ///
103    /// let r = Request::get("/search")
104    ///     .query(slice_params)
105    ///     .query(vec_params)
106    ///     .query(map_params);
107    /// // Result URL: /search?key=value&a=3&b=4&key=another_value
108    /// # }
109    /// ```
110    pub fn query<'a, T, V>(self, params: T) -> Self
111    where
112        T: IntoIterator<Item = (&'a str, V)>,
113        V: AsRef<str>,
114    {
115        for (name, value) in params {
116            self.query.append(name, value.as_ref());
117        }
118        self
119    }
120
121    /// The subresource integrity value of the request (e.g.,
122    /// `sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=`).
123    pub fn integrity(self, integrity: &str) -> Self {
124        self.options.set_integrity(integrity);
125        self
126    }
127
128    /// A convenience method to set JSON as request body
129    ///
130    /// # Note
131    ///
132    /// This method also sets the `Content-Type` header to `application/json`
133    #[cfg(feature = "json")]
134    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
135    pub fn json<T: serde::Serialize + ?Sized>(self, value: &T) -> Result<Request, Error> {
136        let json = serde_json::to_string(value)?;
137        self.header("Content-Type", "application/json").body(json)
138    }
139
140    /// The request method, e.g., GET, POST.
141    pub fn method(self, method: Method) -> Self {
142        self.options.set_method(method.as_ref());
143        self
144    }
145
146    /// The mode you want to use for the request.
147    pub fn mode(self, mode: RequestMode) -> Self {
148        self.options.set_mode(mode);
149        self
150    }
151
152    /// Sets the observer callback.
153    pub fn observe(self, observe: &ObserverCallback) -> Self {
154        self.options.set_observe(observe);
155        self
156    }
157
158    /// How to handle a redirect response:
159    ///
160    /// - *follow*: Automatically follow redirects. Unless otherwise stated the redirect mode is
161    ///   set to follow
162    /// - *error*: Abort with an error if a redirect occurs.
163    /// - *manual*: Caller intends to process the response in another context. See [WHATWG fetch
164    ///   standard](https://fetch.spec.whatwg.org/#requests) for more information.
165    pub fn redirect(self, redirect: RequestRedirect) -> Self {
166        self.options.set_redirect(redirect);
167        self
168    }
169
170    /// The referrer of the request.
171    ///
172    /// This can be a same-origin URL, `about:client`, or an empty string.
173    pub fn referrer(self, referrer: &str) -> Self {
174        self.options.set_referrer(referrer);
175        self
176    }
177
178    /// Specifies the
179    /// [referrer policy](https://w3c.github.io/webappsec-referrer-policy/#referrer-policies) to
180    /// use for the request.
181    pub fn referrer_policy(self, referrer_policy: ReferrerPolicy) -> Self {
182        self.options.set_referrer_policy(referrer_policy);
183        self
184    }
185
186    /// Sets the request abort signal.
187    pub fn abort_signal(self, signal: Option<&AbortSignal>) -> Self {
188        self.options.set_signal(signal);
189        self
190    }
191    /// Builds the request and send it to the server, returning the received response.
192    pub async fn send(self) -> Result<Response, Error> {
193        let req: Request = self.try_into()?;
194        req.send().await
195    }
196    /// Builds the request.
197    pub fn build(self) -> Result<Request, crate::error::Error> {
198        self.try_into()
199    }
200}
201
202impl TryFrom<RequestBuilder> for Request {
203    type Error = crate::error::Error;
204
205    fn try_from(value: RequestBuilder) -> Result<Self, Self::Error> {
206        // To preserve existing query parameters of self.url, it must be parsed and extended with
207        // self.query's parameters. As web_sys::Url just accepts absolute URLs, retrieve the
208        // absolute URL through creating a web_sys::Request object.
209        let request = web_sys::Request::new_with_str(&value.url).map_err(js_to_error)?;
210        let url = web_sys::Url::new(&request.url()).map_err(js_to_error)?;
211
212        let url_search = url.search();
213        let combined_query = if url_search.is_empty() {
214            value.query.to_string()
215        } else {
216            let mut query = value.query.to_string();
217            if !query.is_empty() {
218                query = format!("&{query}");
219            }
220            format!("{url_search}{query}")
221        };
222        url.set_search(&combined_query);
223
224        let final_url = String::from(url.to_string());
225        value.options.set_headers(&value.headers.into_raw());
226        let request = web_sys::Request::new_with_str_and_init(&final_url, &value.options)
227            .map_err(js_to_error)?;
228
229        Ok(request.into())
230    }
231}
232
233impl fmt::Debug for RequestBuilder {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        f.debug_struct("Request").field("url", &self.url).finish()
236    }
237}
238
239/// The [`Request`] sent to the server
240pub struct Request(web_sys::Request);
241
242impl Request {
243    /// Creates a new [`GET`][Method::GET] `Request` with url.
244    pub fn get(url: &str) -> RequestBuilder {
245        RequestBuilder::new(url).method(Method::GET)
246    }
247
248    /// Creates a new [`POST`][Method::POST] `Request` with url.
249    pub fn post(url: &str) -> RequestBuilder {
250        RequestBuilder::new(url).method(Method::POST)
251    }
252
253    /// Creates a new [`PUT`][Method::PUT] `Request` with url.
254    pub fn put(url: &str) -> RequestBuilder {
255        RequestBuilder::new(url).method(Method::PUT)
256    }
257
258    /// Creates a new [`DELETE`][Method::DELETE] `Request` with url.
259    pub fn delete(url: &str) -> RequestBuilder {
260        RequestBuilder::new(url).method(Method::DELETE)
261    }
262
263    /// Creates a new [`PATCH`][Method::PATCH] `Request` with url.
264    pub fn patch(url: &str) -> RequestBuilder {
265        RequestBuilder::new(url).method(Method::PATCH)
266    }
267
268    /// The URL of the request.
269    pub fn url(&self) -> String {
270        self.0.url()
271    }
272
273    /// Gets the headers.
274    pub fn headers(&self) -> Headers {
275        Headers::from_raw(self.0.headers())
276    }
277
278    /// Has the request body been consumed?
279    ///
280    /// If true, then any future attempts to consume the body will error.
281    pub fn body_used(&self) -> bool {
282        self.0.body_used()
283    }
284
285    /// Gets the body.
286    pub fn body(&self) -> Option<ReadableStream> {
287        self.0.body()
288    }
289
290    /// Reads the request to completion, returning it as `FormData`.
291    pub async fn form_data(&self) -> Result<FormData, Error> {
292        let promise = self.0.form_data().map_err(js_to_error)?;
293        let val = JsFuture::from(promise).await.map_err(js_to_error)?;
294        Ok(FormData::from(val))
295    }
296
297    /// Reads the request to completion, parsing it as JSON.
298    #[cfg(feature = "json")]
299    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
300    pub async fn json<T: DeserializeOwned>(&self) -> Result<T, Error> {
301        serde_json::from_str::<T>(&self.text().await?).map_err(Error::from)
302    }
303
304    /// Reads the reqeust as a String.
305    pub async fn text(&self) -> Result<String, Error> {
306        let promise = self.0.text().unwrap();
307        let val = JsFuture::from(promise).await.map_err(js_to_error)?;
308        let string = js_sys::JsString::from(val);
309        Ok(String::from(&string))
310    }
311
312    /// Gets the binary request
313    ///
314    /// This works by obtaining the response as an `ArrayBuffer`, creating a `Uint8Array` from it
315    /// and then converting it to `Vec<u8>`
316    pub async fn binary(&self) -> Result<Vec<u8>, Error> {
317        let promise = self.0.array_buffer().map_err(js_to_error)?;
318        let array_buffer: ArrayBuffer = JsFuture::from(promise)
319            .await
320            .map_err(js_to_error)?
321            .unchecked_into();
322        let typed_buff: Uint8Array = Uint8Array::new(&array_buffer);
323        let mut body = vec![0; typed_buff.length() as usize];
324        typed_buff.copy_to(&mut body);
325        Ok(body)
326    }
327
328    /// Return the read only mode for the request
329    pub fn mode(&self) -> RequestMode {
330        self.0.mode()
331    }
332
333    /// Return the parsed method for the request
334    pub fn method(&self) -> Method {
335        Method::from_str(self.0.method().as_str()).unwrap()
336    }
337
338    /// Executes the request.
339    pub async fn send(self) -> Result<Response, Error> {
340        let request = self.0;
341        let promise = fetch_with_request(&request);
342        let response = JsFuture::from(promise).await.map_err(js_to_error)?;
343        response
344            .dyn_into::<web_sys::Response>()
345            .map_err(|e| panic!("fetch returned {:?}, not `Response` - this is a bug", e))
346            .map(Response::from)
347    }
348}
349
350impl From<web_sys::Request> for Request {
351    fn from(raw: web_sys::Request) -> Self {
352        Request(raw)
353    }
354}
355
356impl From<Request> for web_sys::Request {
357    fn from(val: Request) -> Self {
358        val.0
359    }
360}
361
362impl fmt::Debug for Request {
363    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364        f.debug_struct("Request")
365            .field("url", &self.url())
366            .field("headers", &self.headers())
367            .field("body_used", &self.body_used())
368            .finish()
369    }
370}