dioxus_fullstack_core/
transport.rs

1#![warn(missing_docs)]
2#![doc = include_str!("../README.md")]
3
4use base64::Engine;
5use dioxus_core::CapturedError;
6use serde::{de::DeserializeOwned, Deserialize, Serialize};
7use std::{cell::RefCell, io::Cursor, rc::Rc};
8
9#[cfg(feature = "web")]
10thread_local! {
11    static CONTEXT: RefCell<Option<HydrationContext>> = const { RefCell::new(None) };
12}
13
14/// Data shared between the frontend and the backend for hydration
15/// of server functions.
16#[derive(Default, Clone)]
17pub struct HydrationContext {
18    #[cfg(feature = "web")]
19    /// Is resolving suspense done on the client
20    suspense_finished: bool,
21    data: Rc<RefCell<HTMLData>>,
22}
23
24impl HydrationContext {
25    /// Create a new serialize context from the serialized data
26    pub fn from_serialized(
27        data: &[u8],
28        debug_types: Option<Vec<String>>,
29        debug_locations: Option<Vec<String>>,
30    ) -> Self {
31        Self {
32            #[cfg(feature = "web")]
33            suspense_finished: false,
34            data: Rc::new(RefCell::new(HTMLData::from_serialized(
35                data,
36                debug_types,
37                debug_locations,
38            ))),
39        }
40    }
41
42    /// Serialize the data in the context to be sent to the client
43    pub fn serialized(&self) -> SerializedHydrationData {
44        self.data.borrow().serialized()
45    }
46
47    /// Create a new entry in the data that will be sent to the client without inserting any data. Returns an id that can be used to insert data into the entry once it is ready.
48    pub fn create_entry<T>(&self) -> SerializeContextEntry<T> {
49        let entry_index = self.data.borrow_mut().create_entry();
50
51        SerializeContextEntry {
52            index: entry_index,
53            context: self.clone(),
54            phantom: std::marker::PhantomData,
55        }
56    }
57
58    /// Get the entry for the error in the suspense boundary
59    pub fn error_entry(&self) -> SerializeContextEntry<Option<CapturedError>> {
60        // The first entry is reserved for the error
61        let entry_index = self.data.borrow_mut().create_entry_with_id(0);
62
63        SerializeContextEntry {
64            index: entry_index,
65            context: self.clone(),
66            phantom: std::marker::PhantomData,
67        }
68    }
69
70    /// Extend this data with the data from another [`HydrationContext`]
71    pub fn extend(&self, other: &Self) {
72        self.data.borrow_mut().extend(&other.data.borrow());
73    }
74
75    #[cfg(feature = "web")]
76    /// Run a closure inside of this context
77    pub fn in_context<T>(&self, f: impl FnOnce() -> T) -> T {
78        CONTEXT.with(|context| {
79            let old = context.borrow().clone();
80            *context.borrow_mut() = Some(self.clone());
81            let result = f();
82            *context.borrow_mut() = old;
83            result
84        })
85    }
86
87    pub(crate) fn insert<T: Transportable<M>, M: 'static>(
88        &self,
89        id: usize,
90        value: &T,
91        location: &'static std::panic::Location<'static>,
92    ) {
93        self.data.borrow_mut().insert(id, value, location);
94    }
95
96    pub(crate) fn get<T: Transportable<M>, M: 'static>(
97        &self,
98        id: usize,
99    ) -> Result<T, TakeDataError> {
100        // If suspense is finished on the client, we can assume that the data is available
101        #[cfg(feature = "web")]
102        if self.suspense_finished {
103            return Err(TakeDataError::DataNotAvailable);
104        }
105        self.data.borrow().get(id)
106    }
107}
108
109/// An entry into the serialized context. The order entries are created in must be consistent
110/// between the server and the client.
111pub struct SerializeContextEntry<T> {
112    /// The index this context will be inserted into inside the serialize context
113    index: usize,
114    /// The context this entry is associated with
115    context: HydrationContext,
116    phantom: std::marker::PhantomData<T>,
117}
118
119impl<T> Clone for SerializeContextEntry<T> {
120    fn clone(&self) -> Self {
121        Self {
122            index: self.index,
123            context: self.context.clone(),
124            phantom: std::marker::PhantomData,
125        }
126    }
127}
128
129impl<T> SerializeContextEntry<T> {
130    /// Insert data into an entry that was created with [`HydrationContext::create_entry`]
131    pub fn insert<M: 'static>(self, value: &T, location: &'static std::panic::Location<'static>)
132    where
133        T: Transportable<M>,
134    {
135        self.context.insert(self.index, value, location);
136    }
137
138    /// Grab the data from the serialize context
139    pub fn get<M: 'static>(&self) -> Result<T, TakeDataError>
140    where
141        T: Transportable<M>,
142    {
143        self.context.get(self.index)
144    }
145}
146
147/// Check if the client is currently rendering a component for hydration. Always returns true on the server.
148pub fn is_hydrating() -> bool {
149    #[cfg(feature = "web")]
150    {
151        // On the client, we can check if the context is set
152        CONTEXT.with(|context| context.borrow().is_some())
153    }
154    #[cfg(not(feature = "web"))]
155    {
156        true
157    }
158}
159
160/// Get or insert the current serialize context. On the client, the hydration context this returns
161/// will always return `TakeDataError::DataNotAvailable` if hydration of the current chunk is finished.
162pub fn serialize_context() -> HydrationContext {
163    #[cfg(feature = "web")]
164    // On the client, the hydration logic provides the context in a global
165    if let Some(current_context) = CONTEXT.with(|context| context.borrow().clone()) {
166        current_context
167    } else {
168        // If the context is not set, then suspense is not active
169        HydrationContext {
170            suspense_finished: true,
171            ..Default::default()
172        }
173    }
174    #[cfg(not(feature = "web"))]
175    {
176        // On the server each scope creates the context lazily
177        dioxus_core::has_context()
178            .unwrap_or_else(|| dioxus_core::provide_context(HydrationContext::default()))
179    }
180}
181
182pub(crate) struct HTMLData {
183    /// The position of the cursor in the data. This is only used on the client
184    pub(crate) cursor: usize,
185    /// The data required for hydration
186    pub data: Vec<Option<Vec<u8>>>,
187    /// The types of each serialized data
188    ///
189    /// NOTE: we don't store this in the main data vec because we don't want to include it in
190    /// release mode and we can't assume both the client and server are built with debug assertions
191    /// matching
192    #[cfg(debug_assertions)]
193    pub debug_types: Vec<Option<String>>,
194    /// The locations of each serialized data
195    #[cfg(debug_assertions)]
196    pub debug_locations: Vec<Option<String>>,
197}
198
199impl Default for HTMLData {
200    fn default() -> Self {
201        Self {
202            cursor: 1,
203            data: Vec::new(),
204            #[cfg(debug_assertions)]
205            debug_types: Vec::new(),
206            #[cfg(debug_assertions)]
207            debug_locations: Vec::new(),
208        }
209    }
210}
211
212impl HTMLData {
213    #[allow(unused)]
214    fn from_serialized(
215        data: &[u8],
216        debug_types: Option<Vec<String>>,
217        debug_locations: Option<Vec<String>>,
218    ) -> Self {
219        let data = ciborium::from_reader(Cursor::new(data)).unwrap();
220        Self {
221            cursor: 1,
222            data,
223            #[cfg(debug_assertions)]
224            debug_types: debug_types
225                .unwrap_or_default()
226                .into_iter()
227                .map(Some)
228                .collect(),
229            #[cfg(debug_assertions)]
230            debug_locations: debug_locations
231                .unwrap_or_default()
232                .into_iter()
233                .map(Some)
234                .collect(),
235        }
236    }
237
238    /// Create a new entry in the data that will be sent to the client without inserting any data. Returns an id that can be used to insert data into the entry once it is ready.
239    fn create_entry(&mut self) -> usize {
240        let id = self.cursor;
241        self.cursor += 1;
242        self.create_entry_with_id(id)
243    }
244
245    fn create_entry_with_id(&mut self, id: usize) -> usize {
246        while id + 1 > self.data.len() {
247            self.data.push(None);
248            #[cfg(debug_assertions)]
249            {
250                self.debug_types.push(None);
251                self.debug_locations.push(None);
252            }
253        }
254        id
255    }
256
257    /// Insert data into an entry that was created with [`Self::create_entry`]
258    fn insert<T: Transportable<M>, M: 'static>(
259        &mut self,
260        id: usize,
261        value: &T,
262        #[allow(unused)] location: &'static std::panic::Location<'static>,
263    ) {
264        let serialized = value.transport_to_bytes();
265        self.data[id] = Some(serialized);
266        #[cfg(debug_assertions)]
267        {
268            self.debug_types[id] = Some(std::any::type_name::<T>().to_string());
269            self.debug_locations[id] = Some(location.to_string());
270        }
271    }
272
273    /// Get the data from the serialize context
274    fn get<T: Transportable<M>, M: 'static>(&self, index: usize) -> Result<T, TakeDataError> {
275        if index >= self.data.len() {
276            tracing::trace!(
277                "Tried to take more data than was available, len: {}, index: {}; This is normal if the server function was started on the client, but may indicate a bug if the server function result should be deserialized from the server",
278                self.data.len(),
279                index
280            );
281            return Err(TakeDataError::DataNotAvailable);
282        }
283        let bytes = self.data[index].as_ref();
284        match bytes {
285            Some(bytes) => match T::transport_from_bytes(bytes) {
286                Ok(x) => Ok(x),
287                Err(err) => {
288                    #[cfg(debug_assertions)]
289                    {
290                        let debug_type = self.debug_types.get(index);
291                        let debug_locations = self.debug_locations.get(index);
292
293                        if let (Some(Some(debug_type)), Some(Some(debug_locations))) =
294                            (debug_type, debug_locations)
295                        {
296                            let client_type = std::any::type_name::<T>();
297                            let client_location = std::panic::Location::caller();
298                            // We we have debug types and a location, we can provide a more helpful error message
299                            tracing::error!(
300                                "Error deserializing data: {err:?}\n\nThis type was serialized on the server at {debug_locations} with the type name {debug_type}. The client failed to deserialize the type {client_type} at {client_location}.",
301                            );
302                            return Err(TakeDataError::DeserializationError(err));
303                        }
304                    }
305                    // Otherwise, just log the generic deserialization error
306                    tracing::error!("Error deserializing data: {:?}", err);
307                    Err(TakeDataError::DeserializationError(err))
308                }
309            },
310            None => Err(TakeDataError::DataPending),
311        }
312    }
313
314    /// Extend this data with the data from another [`HTMLData`]
315    pub(crate) fn extend(&mut self, other: &Self) {
316        // Make sure this vectors error entry exists even if it is empty
317        if self.data.is_empty() {
318            self.data.push(None);
319            #[cfg(debug_assertions)]
320            {
321                self.debug_types.push(None);
322                self.debug_locations.push(None);
323            }
324        }
325
326        let mut other_data_iter = other.data.iter().cloned();
327        #[cfg(debug_assertions)]
328        let mut other_debug_types_iter = other.debug_types.iter().cloned();
329        #[cfg(debug_assertions)]
330        let mut other_debug_locations_iter = other.debug_locations.iter().cloned();
331
332        // Merge the error entry from the other context
333        if let Some(Some(other_error)) = other_data_iter.next() {
334            self.data[0] = Some(other_error.clone());
335            #[cfg(debug_assertions)]
336            {
337                self.debug_types[0] = other_debug_types_iter.next().unwrap_or(None);
338                self.debug_locations[0] = other_debug_locations_iter.next().unwrap_or(None);
339            }
340        }
341
342        // Don't copy the error from the other context
343        self.data.extend(other_data_iter);
344        #[cfg(debug_assertions)]
345        {
346            self.debug_types.extend(other_debug_types_iter);
347            self.debug_locations.extend(other_debug_locations_iter);
348        }
349    }
350
351    /// Encode data as base64. This is intended to be used in the server to send data to the client.
352    pub(crate) fn serialized(&self) -> SerializedHydrationData {
353        let mut serialized = Vec::new();
354        ciborium::into_writer(&self.data, &mut serialized).unwrap();
355
356        let data = base64::engine::general_purpose::STANDARD.encode(serialized);
357
358        #[cfg(debug_assertions)]
359        let format_js_list_of_strings = |list: &[Option<String>]| {
360            let body = list
361                .iter()
362                .map(|s| match s {
363                    Some(s) => {
364                        // Escape backslashes, quotes, and newlines
365                        let escaped = s
366                            .replace(r#"\"#, r#"\\"#)
367                            .replace("\n", r#"\n"#)
368                            .replace(r#"""#, r#"\""#);
369
370                        format!(r#""{escaped}""#)
371                    }
372                    None => r#""unknown""#.to_string(),
373                })
374                .collect::<Vec<_>>()
375                .join(",");
376            format!("[{}]", body)
377        };
378
379        SerializedHydrationData {
380            data,
381            #[cfg(debug_assertions)]
382            debug_types: format_js_list_of_strings(&self.debug_types),
383            #[cfg(debug_assertions)]
384            debug_locations: format_js_list_of_strings(&self.debug_locations),
385        }
386    }
387}
388
389/// Data that was serialized on the server for hydration on the client. This includes
390/// extra information about the types and sources of the serialized data in debug mode
391pub struct SerializedHydrationData {
392    /// The base64 encoded serialized data
393    pub data: String,
394    /// A list of the types of each serialized data
395    #[cfg(debug_assertions)]
396    pub debug_types: String,
397    /// A list of the locations of each serialized data
398    #[cfg(debug_assertions)]
399    pub debug_locations: String,
400}
401
402/// An error that can occur when trying to take data from the server
403#[derive(Debug)]
404pub enum TakeDataError {
405    /// Deserializing the data failed
406    DeserializationError(ciborium::de::Error<std::io::Error>),
407    /// No data was available
408    DataNotAvailable,
409    /// The server serialized a placeholder for the data, but it isn't available yet
410    DataPending,
411}
412
413impl std::fmt::Display for TakeDataError {
414    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
415        match self {
416            Self::DeserializationError(e) => write!(f, "DeserializationError: {}", e),
417            Self::DataNotAvailable => write!(f, "DataNotAvailable"),
418            Self::DataPending => write!(f, "DataPending"),
419        }
420    }
421}
422
423impl std::error::Error for TakeDataError {}
424
425/// Create a new entry in the serialize context for the head element hydration
426pub fn head_element_hydration_entry() -> SerializeContextEntry<bool> {
427    serialize_context().create_entry()
428}
429
430/// A `Transportable` type can be safely transported from the server to the client, and be used for
431/// hydration. Not all types can sensibly be transported, but many can. This trait makes it possible
432/// to customize how types are transported which helps for non-serializable types like `dioxus_core::CapturedError`.
433///
434/// By default, all types that implement `Serialize` and `DeserializeOwned` are transportable.
435///
436/// You can also implement `Transportable` for `Result<T, dioxus_core::CapturedError>` where `T` is
437/// `Serialize` and `DeserializeOwned` to allow transporting results that may contain errors.
438///
439/// Note that transporting a `Result<T, dioxus_core::CapturedError>` will lose various aspects of the original
440/// `dioxus_core::CapturedError` such as backtraces and source errors, but will preserve the error message.
441pub trait Transportable<M = ()>: 'static {
442    /// Serialize the type to a byte vector for transport
443    fn transport_to_bytes(&self) -> Vec<u8>;
444
445    /// Deserialize the type from a byte slice
446    fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
447    where
448        Self: Sized;
449}
450
451impl<T> Transportable<()> for T
452where
453    T: Serialize + DeserializeOwned + 'static,
454{
455    fn transport_to_bytes(&self) -> Vec<u8> {
456        let mut serialized = Vec::new();
457        ciborium::into_writer(self, &mut serialized).unwrap();
458        serialized
459    }
460
461    fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
462    where
463        Self: Sized,
464    {
465        ciborium::from_reader(Cursor::new(bytes))
466    }
467}
468
469#[derive(Serialize, Deserialize)]
470struct TransportResultErr<T> {
471    error: Result<T, CapturedError>,
472}
473
474#[doc(hidden)]
475pub struct TransportViaErrMarker;
476
477impl<T> Transportable<TransportViaErrMarker> for Result<T, anyhow::Error>
478where
479    T: Serialize + DeserializeOwned + 'static,
480{
481    fn transport_to_bytes(&self) -> Vec<u8> {
482        let err = TransportResultErr {
483            error: self
484                .as_ref()
485                .map_err(|e| CapturedError::from_display(e.to_string())),
486        };
487
488        let mut serialized = Vec::new();
489        ciborium::into_writer(&err, &mut serialized).unwrap();
490        serialized
491    }
492
493    fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
494    where
495        Self: Sized,
496    {
497        let err: TransportResultErr<T> = ciborium::from_reader(Cursor::new(bytes))?;
498        match err.error {
499            Ok(value) => Ok(Ok(value)),
500            Err(captured) => Ok(Err(anyhow::Error::msg(captured.to_string()))),
501        }
502    }
503}
504
505#[doc(hidden)]
506pub struct TransportCapturedError;
507#[derive(Serialize, Deserialize)]
508struct TransportError {
509    error: String,
510}
511
512impl Transportable<TransportCapturedError> for CapturedError {
513    fn transport_to_bytes(&self) -> Vec<u8> {
514        let err = TransportError {
515            error: self.to_string(),
516        };
517
518        let mut serialized = Vec::new();
519        ciborium::into_writer(&err, &mut serialized).unwrap();
520        serialized
521    }
522
523    fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
524    where
525        Self: Sized,
526    {
527        let err: TransportError = ciborium::from_reader(Cursor::new(bytes))?;
528        Ok(dioxus_core::CapturedError::msg::<String>(err.error))
529    }
530}