Skip to main content

dioxus_fullstack/
client.rs

1#![allow(unreachable_code)]
2
3use crate::{StreamingError, reqwest_error_to_request_error};
4use bytes::Bytes;
5use dioxus_fullstack_core::RequestError;
6use futures::Stream;
7use futures::{TryFutureExt, TryStreamExt};
8use headers::{ContentType, Header};
9use http::{Extensions, HeaderMap, HeaderName, HeaderValue, Method, StatusCode, response::Parts};
10use send_wrapper::SendWrapper;
11use serde::{Serialize, de::DeserializeOwned};
12use std::sync::{LazyLock, Mutex, OnceLock, RwLock};
13use std::{fmt::Display, pin::Pin, prelude::rust_2024::Future};
14use url::Url;
15
16pub static GLOBAL_REQUEST_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
17
18/// Installs _ring_ as the process-wide rustls crypto provider, unless one is already installed.
19///
20/// Fullstack enables reqwest's `rustls-no-provider` feature, so without a process-wide provider
21/// any other `reqwest::Client` built in the app (e.g. `reqwest::get`) would panic.
22#[cfg(not(target_arch = "wasm32"))]
23#[doc(hidden)]
24pub fn install_default_crypto_provider() {
25    let _ = rustls::crypto::ring::default_provider().install_default();
26}
27
28/// TLS config for the server function client: _ring_ with the bundled webpki roots, so it works
29/// without platform verifier setup (which Android requires).
30#[cfg(not(target_arch = "wasm32"))]
31fn rustls_client_config() -> rustls::ClientConfig {
32    let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
33    let mut config = rustls::ClientConfig::builder_with_provider(std::sync::Arc::new(
34        rustls::crypto::ring::default_provider(),
35    ))
36    .with_safe_default_protocol_versions()
37    .expect("ring supports the default TLS versions")
38    .with_root_certificates(roots)
39    .with_no_client_auth();
40    // Websocket upgrades require HTTP/1.1
41    config.alpn_protocols = vec![b"http/1.1".to_vec()];
42    config
43}
44
45pub type ClientResult = Result<ClientResponse, RequestError>;
46
47pub struct ClientRequest {
48    pub url: Url,
49    pub headers: HeaderMap,
50    pub method: Method,
51    pub extensions: Extensions,
52}
53
54impl ClientRequest {
55    /// Create a new ClientRequest with the given method, url path, and query parameters.
56    pub fn new(method: http::Method, path: String, params: &impl Serialize) -> Self {
57        Self::fetch_inner(method, path, serde_qs::to_string(params).unwrap())
58    }
59
60    // Shrink monomorphization bloat by moving this to its own function
61    fn fetch_inner(method: http::Method, path: String, query: String) -> ClientRequest {
62        // On wasm, this doesn't matter since we always use relative URLs when making requests anyways
63        let mut server_url = get_server_url();
64
65        if server_url.is_empty() {
66            server_url = "http://this.is.not.a.real.url:9000";
67        }
68
69        let url = format!(
70            "{server_url}{path}{params}",
71            params = if query.is_empty() {
72                "".to_string()
73            } else {
74                format!("?{}", query)
75            }
76        )
77        .parse()
78        .unwrap();
79
80        let headers = get_request_headers();
81
82        ClientRequest {
83            method,
84            url,
85            headers,
86            extensions: Extensions::new(),
87        }
88    }
89
90    /// Get the HTTP method of this Request.
91    pub fn method(&self) -> &Method {
92        &self.method
93    }
94
95    pub fn url(&self) -> &Url {
96        &self.url
97    }
98
99    /// Extend the query parameters of this request with the given serialzable struct.
100    ///
101    /// This will use `serde_qs` to serialize the struct into query parameters. `serde_qs` has various
102    /// restrictions - make sure to read its documentation!
103    pub fn extend_query(mut self, query: &impl Serialize) -> Self {
104        let old_query = self.url.query().unwrap_or("");
105        let new_query = serde_qs::to_string(query).unwrap();
106        let combined_query = format!(
107            "{}{}{}",
108            old_query,
109            if old_query.is_empty() { "" } else { "&" },
110            new_query
111        );
112        self.url.set_query(Some(&combined_query));
113        self
114    }
115
116    /// Add a `Header` to this Request.
117    pub fn header(
118        mut self,
119        name: impl TryInto<HeaderName, Error = impl Display>,
120        value: impl TryInto<HeaderValue, Error = impl Display>,
121    ) -> Result<Self, RequestError> {
122        self.headers.append(
123            name.try_into()
124                .map_err(|d| RequestError::Builder(d.to_string()))?,
125            value
126                .try_into()
127                .map_err(|d| RequestError::Builder(d.to_string()))?,
128        );
129        Ok(self)
130    }
131
132    /// Add a `Header` to this Request.
133    pub fn typed_header<H: Header>(mut self, header: H) -> Self {
134        let mut headers = vec![];
135        header.encode(&mut headers);
136        for header in headers {
137            self.headers.append(H::name(), header);
138        }
139        self
140    }
141
142    /// Creates a new reqwest client with cookies set
143    pub fn new_reqwest_client() -> reqwest::Client {
144        #[allow(unused_mut)]
145        let mut client = reqwest::Client::builder();
146
147        #[cfg(not(target_arch = "wasm32"))]
148        {
149            use std::sync::Arc;
150            use std::sync::LazyLock;
151
152            static COOKIES: LazyLock<Arc<reqwest::cookie::Jar>> =
153                LazyLock::new(|| Arc::new(reqwest::cookie::Jar::default()));
154
155            client = client
156                .cookie_store(true)
157                .cookie_provider(COOKIES.clone())
158                .tls_backend_preconfigured(rustls_client_config());
159        }
160
161        client.build().unwrap()
162    }
163
164    /// Creates a new reqwest request builder with the method, url, and headers set from this ClientRequest
165    ///
166    /// Using this method attaches `X-Request-Client: dioxus` header to the request.
167    pub fn new_reqwest_request(&self) -> reqwest::RequestBuilder {
168        let client = GLOBAL_REQUEST_CLIENT.get_or_init(Self::new_reqwest_client);
169
170        let mut req = client
171            .request(self.method.clone(), self.url.clone())
172            .header("X-Request-Client", "dioxus");
173
174        for (key, value) in self.headers.iter() {
175            req = req.header(key, value);
176        }
177
178        req
179    }
180
181    /// Using this method attaches `X-Request-Client-Dioxus` header to the request.
182    #[cfg(feature = "web")]
183    pub fn new_gloo_request(&self) -> gloo_net::http::RequestBuilder {
184        let mut builder = gloo_net::http::RequestBuilder::new(
185            format!(
186                "{path}{query_string}",
187                path = self.url.path(),
188                query_string = self
189                    .url
190                    .query()
191                    .map(|query| format!("?{query}"))
192                    .unwrap_or_default()
193            )
194            .as_str(),
195        )
196        .header("X-Request-Client", "dioxus")
197        .method(self.method.clone());
198
199        for (key, value) in self.headers.iter() {
200            let value = match value.to_str() {
201                Ok(v) => v,
202                Err(er) => {
203                    tracing::error!("Error converting header {key} value: {}", er);
204                    continue;
205                }
206            };
207
208            builder = builder.header(key.as_str(), value);
209        }
210
211        builder
212    }
213
214    /// Sends the request with multipart/form-data body constructed from the given FormData.
215    #[cfg(not(target_arch = "wasm32"))]
216    pub async fn send_multipart(
217        self,
218        form: &dioxus_html::FormData,
219    ) -> Result<ClientResponse, RequestError> {
220        let mut outgoing = reqwest::multipart::Form::new();
221
222        for (key, value) in form.values() {
223            match value {
224                dioxus_html::FormValue::Text(text) => {
225                    outgoing = outgoing.text(key.to_string(), text.to_string());
226                }
227                dioxus_html::FormValue::File(Some(file_data)) => {
228                    outgoing = outgoing
229                        .file(key.to_string(), file_data.path())
230                        .await
231                        .map_err(|e| {
232                            RequestError::Builder(format!(
233                                "Failed to add file to multipart form: {e}",
234                            ))
235                        })?;
236                }
237                dioxus_html::FormValue::File(None) => {
238                    // No file was selected for this input, so we skip it.
239                    outgoing = outgoing.part(key.to_string(), reqwest::multipart::Part::bytes(b""));
240                }
241            }
242        }
243
244        let res = self
245            .new_reqwest_request()
246            .multipart(outgoing)
247            .send()
248            .await
249            .map_err(reqwest_error_to_request_error)?;
250
251        Ok(ClientResponse {
252            response: Box::new(res),
253            extensions: self.extensions,
254        })
255    }
256
257    pub async fn send_form(self, data: &impl Serialize) -> Result<ClientResponse, RequestError> {
258        // For GET and HEAD requests, we encode the form data as query parameters.
259        // For other request methods, we encode the form data as the request body.
260        if matches!(*self.method(), Method::GET | Method::HEAD) {
261            return self.extend_query(data).send_empty_body().await;
262        }
263
264        let body =
265            serde_urlencoded::to_string(data).map_err(|err| RequestError::Body(err.to_string()))?;
266
267        self.typed_header(ContentType::form_url_encoded())
268            .send_raw_bytes(body)
269            .await
270    }
271
272    /// Sends the request with an empty body.
273    pub async fn send_empty_body(self) -> Result<ClientResponse, RequestError> {
274        #[cfg(feature = "web")]
275        if cfg!(target_arch = "wasm32") {
276            return self.send_js_value(wasm_bindgen::JsValue::UNDEFINED).await;
277        }
278
279        #[cfg(not(target_arch = "wasm32"))]
280        {
281            let res = self
282                .new_reqwest_request()
283                .send()
284                .await
285                .map_err(reqwest_error_to_request_error)?;
286
287            return Ok(ClientResponse {
288                response: Box::new(res),
289                extensions: self.extensions,
290            });
291        }
292
293        unimplemented!()
294    }
295
296    pub async fn send_raw_bytes(
297        self,
298        bytes: impl Into<Bytes>,
299    ) -> Result<ClientResponse, RequestError> {
300        #[cfg(feature = "web")]
301        if cfg!(target_arch = "wasm32") {
302            let bytes = bytes.into();
303            let uint_8_array = js_sys::Uint8Array::from(&bytes[..]);
304            return self.send_js_value(uint_8_array.into()).await;
305        }
306
307        #[cfg(not(target_arch = "wasm32"))]
308        {
309            let res = self
310                .new_reqwest_request()
311                .body(bytes.into())
312                .send()
313                .await
314                .map_err(reqwest_error_to_request_error)?;
315
316            return Ok(ClientResponse {
317                response: Box::new(res),
318                extensions: self.extensions,
319            });
320        }
321
322        unimplemented!()
323    }
324
325    /// Sends text data with the `text/plain; charset=utf-8` content type.
326    pub async fn send_text(
327        self,
328        text: impl Into<String> + Into<Bytes>,
329    ) -> Result<ClientResponse, RequestError> {
330        self.typed_header(ContentType::text_utf8())
331            .send_raw_bytes(text)
332            .await
333    }
334
335    /// Sends JSON data with the `application/json` content type.
336    pub async fn send_json(self, json: &impl Serialize) -> Result<ClientResponse, RequestError> {
337        let bytes =
338            serde_json::to_vec(json).map_err(|e| RequestError::Serialization(e.to_string()))?;
339
340        if bytes.is_empty() || bytes == b"{}" || bytes == b"null" {
341            return self.send_empty_body().await;
342        }
343
344        self.typed_header(ContentType::json())
345            .send_raw_bytes(bytes)
346            .await
347    }
348
349    pub async fn send_body_stream(
350        self,
351        stream: impl Stream<Item = Result<Bytes, StreamingError>> + Send + 'static,
352    ) -> Result<ClientResponse, RequestError> {
353        #[cfg(not(target_arch = "wasm32"))]
354        {
355            let res = self
356                .new_reqwest_request()
357                .body(reqwest::Body::wrap_stream(stream))
358                .send()
359                .await
360                .map_err(reqwest_error_to_request_error)?;
361
362            return Ok(ClientResponse {
363                response: Box::new(res),
364                extensions: self.extensions,
365            });
366        }
367
368        // On the web, we have to buffer the entire stream into a Blob before sending it,
369        // since the Fetch API doesn't support streaming request bodies on browsers yet.
370        #[cfg(feature = "web")]
371        {
372            use wasm_bindgen::JsValue;
373
374            let stream: Vec<Bytes> = stream.try_collect().await.map_err(|e| {
375                RequestError::Request(format!("Error collecting stream for request body: {}", e))
376            })?;
377
378            let uint_8_array =
379                js_sys::Uint8Array::new_with_length(stream.iter().map(|b| b.len() as u32).sum());
380
381            let mut offset = 0;
382            for chunk in stream {
383                uint_8_array.set(&js_sys::Uint8Array::from(&chunk[..]), offset);
384                offset += chunk.len() as u32;
385            }
386
387            return self.send_js_value(JsValue::from(uint_8_array)).await;
388        }
389
390        unimplemented!()
391    }
392
393    #[cfg(feature = "web")]
394    pub async fn send_js_value(
395        self,
396        value: wasm_bindgen::JsValue,
397    ) -> Result<ClientResponse, RequestError> {
398        use std::str::FromStr;
399
400        let inner = self
401            .new_gloo_request()
402            .body(value)
403            .map_err(|e| RequestError::Request(e.to_string()))?
404            .send()
405            .await
406            .map_err(|e| RequestError::Request(e.to_string()))?;
407
408        let status = inner.status();
409        let url = inner
410            .url()
411            .parse()
412            .map_err(|e| RequestError::Request(format!("Error parsing response URL: {}", e)))?;
413
414        let headers = {
415            let mut map = HeaderMap::new();
416            for (key, value) in inner.headers().entries() {
417                if let Ok(header_value) = http::HeaderValue::from_str(&value) {
418                    let header = HeaderName::from_str(&key).unwrap();
419                    map.append(header, header_value);
420                }
421            }
422            map
423        };
424
425        let content_length = headers
426            .get(http::header::CONTENT_LENGTH)
427            .and_then(|val| val.to_str().ok())
428            .and_then(|s| s.parse::<u64>().ok());
429
430        let status = http::StatusCode::from_u16(status).unwrap_or(http::StatusCode::OK);
431
432        Ok(ClientResponse {
433            extensions: self.extensions,
434            response: Box::new(browser::WrappedGlooResponse {
435                inner,
436                headers,
437                status,
438                url,
439                content_length,
440            }),
441        })
442    }
443}
444
445// On wasm reqwest not being send/sync gets annoying, but it's not relevant since wasm is single-threaded
446unsafe impl Send for ClientRequest {}
447unsafe impl Sync for ClientRequest {}
448
449/// A wrapper type over the platform's HTTP response type.
450///
451/// This abstracts over the inner `reqwest::Response` type and provides the original request
452/// and a way to store state associated with the response.
453///
454/// On the web, it uses `web_sys::Response` instead of `reqwest::Response` to avoid pulling in
455/// the entire `reqwest` crate and to support native browser APIs.
456pub struct ClientResponse {
457    pub(crate) response: Box<dyn ClientResponseDriver>,
458    pub(crate) extensions: Extensions,
459}
460
461impl ClientResponse {
462    pub fn status(&self) -> StatusCode {
463        self.response.status()
464    }
465
466    pub fn headers(&self) -> &HeaderMap {
467        self.response.headers()
468    }
469
470    pub fn url(&self) -> &Url {
471        self.response.url()
472    }
473
474    pub fn content_length(&self) -> Option<u64> {
475        self.response.content_length()
476    }
477
478    pub async fn bytes(self) -> Result<Bytes, RequestError> {
479        self.response.bytes().await
480    }
481
482    pub fn bytes_stream(
483        self,
484    ) -> impl futures_util::Stream<Item = Result<Bytes, StreamingError>> + 'static + Unpin + Send
485    {
486        self.response.bytes_stream()
487    }
488
489    pub fn extensions(&self) -> &Extensions {
490        &self.extensions
491    }
492
493    pub fn extensions_mut(&mut self) -> &mut Extensions {
494        &mut self.extensions
495    }
496
497    pub async fn json<T: DeserializeOwned>(self) -> Result<T, RequestError> {
498        serde_json::from_slice(&self.bytes().await?)
499            .map_err(|e| RequestError::Decode(e.to_string()))
500    }
501
502    pub async fn text(self) -> Result<String, RequestError> {
503        self.response.text().await
504    }
505
506    /// Creates the `http::response::Parts` from this response.
507    pub fn make_parts(&self) -> Parts {
508        let mut response = http::response::Response::builder().status(self.response.status());
509
510        response = response.version(self.response.version());
511
512        for (key, value) in self.response.headers().iter() {
513            response = response.header(key, value);
514        }
515
516        let (parts, _) = response.body(()).unwrap().into_parts();
517
518        parts
519    }
520
521    /// Consumes the response, returning the head and a stream of the body.
522    pub fn into_parts(self) -> (Parts, impl Stream<Item = Result<Bytes, StreamingError>>) {
523        (self.make_parts(), self.bytes_stream())
524    }
525}
526
527/// Set the root server URL that all server function paths are relative to for the client.
528///
529/// If this is not set, it defaults to the origin.
530///
531/// This may be called more than once; the most recent value wins. Requests read
532/// the URL as they are built, so a change applies to the next server function
533/// call. Clients that talk to more than one backend can switch between them
534/// without restarting the process.
535pub fn set_server_url(url: &'static str) {
536    *ROOT_URL.write().unwrap() = Some(url);
537}
538
539/// Returns the root server URL for all server functions.
540pub fn get_server_url() -> &'static str {
541    ROOT_URL.read().unwrap().unwrap_or("")
542}
543
544static ROOT_URL: RwLock<Option<&'static str>> = RwLock::new(None);
545
546/// Delete the extra request headers for all server functions.
547pub fn clear_request_headers() {
548    REQUEST_HEADERS.lock().unwrap().clear();
549}
550
551/// Set the extra request headers for all server functions.
552pub fn set_request_headers(headers: HeaderMap) {
553    *REQUEST_HEADERS.lock().unwrap() = headers;
554}
555
556/// Returns the extra request headers for all server functions.
557pub fn get_request_headers() -> HeaderMap {
558    REQUEST_HEADERS.lock().unwrap().clone()
559}
560
561static REQUEST_HEADERS: LazyLock<Mutex<HeaderMap>> = LazyLock::new(|| Mutex::new(HeaderMap::new()));
562
563pub trait ClientResponseDriver {
564    fn status(&self) -> StatusCode;
565    fn headers(&self) -> &HeaderMap;
566    fn url(&self) -> &Url;
567    fn version(&self) -> http::Version {
568        http::Version::HTTP_2
569    }
570    fn content_length(&self) -> Option<u64>;
571    fn bytes(self: Box<Self>) -> Pin<Box<dyn Future<Output = Result<Bytes, RequestError>> + Send>>;
572    fn bytes_stream(
573        self: Box<Self>,
574    ) -> Pin<Box<dyn Stream<Item = Result<Bytes, StreamingError>> + 'static + Unpin + Send>>;
575
576    fn text(self: Box<Self>) -> Pin<Box<dyn Future<Output = Result<String, RequestError>> + Send>>;
577}
578
579mod native {
580    use futures::Stream;
581
582    use super::*;
583
584    impl ClientResponseDriver for reqwest::Response {
585        fn status(&self) -> http::StatusCode {
586            reqwest::Response::status(self)
587        }
588
589        fn version(&self) -> http::Version {
590            #[cfg(target_arch = "wasm32")]
591            {
592                return http::Version::HTTP_2;
593            }
594
595            reqwest::Response::version(self)
596        }
597
598        fn headers(&self) -> &http::HeaderMap {
599            reqwest::Response::headers(self)
600        }
601
602        fn url(&self) -> &url::Url {
603            reqwest::Response::url(self)
604        }
605
606        fn content_length(&self) -> Option<u64> {
607            reqwest::Response::content_length(self)
608        }
609
610        fn bytes(
611            self: Box<Self>,
612        ) -> Pin<Box<dyn Future<Output = Result<Bytes, RequestError>> + Send>> {
613            Box::pin(SendWrapper::new(async move {
614                reqwest::Response::bytes(*self)
615                    .map_err(reqwest_error_to_request_error)
616                    .await
617            }))
618        }
619
620        fn bytes_stream(
621            self: Box<Self>,
622        ) -> Pin<Box<dyn Stream<Item = Result<Bytes, StreamingError>> + 'static + Unpin + Send>>
623        {
624            Box::pin(SendWrapper::new(
625                reqwest::Response::bytes_stream(*self).map_err(|_| StreamingError::Failed),
626            ))
627        }
628
629        fn text(
630            self: Box<Self>,
631        ) -> Pin<Box<dyn Future<Output = Result<String, RequestError>> + Send>> {
632            Box::pin(SendWrapper::new(async move {
633                reqwest::Response::text(*self)
634                    .map_err(reqwest_error_to_request_error)
635                    .await
636            }))
637        }
638    }
639}
640
641#[cfg(feature = "web")]
642mod browser {
643    use crate::{ClientResponseDriver, StreamingError};
644    use bytes::Bytes;
645    use dioxus_fullstack_core::RequestError;
646    use futures::{Stream, StreamExt};
647    use http::{HeaderMap, StatusCode};
648    use js_sys::Uint8Array;
649    use send_wrapper::SendWrapper;
650    use std::{pin::Pin, prelude::rust_2024::Future};
651    use wasm_bindgen::JsCast;
652
653    pub(crate) struct WrappedGlooResponse {
654        pub(crate) inner: gloo_net::http::Response,
655        pub(crate) headers: HeaderMap,
656        pub(crate) status: StatusCode,
657        pub(crate) url: url::Url,
658        pub(crate) content_length: Option<u64>,
659    }
660
661    impl ClientResponseDriver for WrappedGlooResponse {
662        fn status(&self) -> StatusCode {
663            self.status
664        }
665
666        fn headers(&self) -> &HeaderMap {
667            &self.headers
668        }
669
670        fn url(&self) -> &url::Url {
671            &self.url
672        }
673
674        fn content_length(&self) -> Option<u64> {
675            self.content_length
676        }
677
678        fn bytes(
679            self: Box<Self>,
680        ) -> Pin<Box<dyn Future<Output = Result<Bytes, RequestError>> + Send>> {
681            Box::pin(SendWrapper::new(async move {
682                let bytes = self
683                    .inner
684                    .binary()
685                    .await
686                    .map_err(|e| RequestError::Request(e.to_string()))?;
687                Ok(bytes.into())
688            }))
689        }
690
691        fn bytes_stream(
692            self: Box<Self>,
693        ) -> Pin<Box<dyn Stream<Item = Result<Bytes, StreamingError>> + 'static + Unpin + Send>>
694        {
695            let body = match self.inner.body() {
696                Some(body) => body,
697                None => {
698                    return Box::pin(SendWrapper::new(futures::stream::iter([Err(
699                        StreamingError::Failed,
700                    )])));
701                }
702            };
703
704            Box::pin(SendWrapper::new(
705                wasm_streams::ReadableStream::from_raw(body)
706                    .into_stream()
707                    .map(|chunk| {
708                        let array = chunk
709                            .map_err(|_| StreamingError::Failed)?
710                            .dyn_into::<Uint8Array>()
711                            .map_err(|_| StreamingError::Failed)?;
712                        Ok(array.to_vec().into())
713                    }),
714            ))
715        }
716
717        fn text(
718            self: Box<Self>,
719        ) -> Pin<Box<dyn Future<Output = Result<String, RequestError>> + Send>> {
720            Box::pin(SendWrapper::new(async move {
721                self.inner
722                    .text()
723                    .await
724                    .map_err(|e| RequestError::Request(e.to_string()))
725            }))
726        }
727    }
728}