dioxus_fullstack_core/
streaming.rs

1use crate::{HttpError, ServerFnError};
2use axum_core::{extract::FromRequest, response::IntoResponse};
3use dioxus_core::{try_consume_context, CapturedError};
4use dioxus_signals::{ReadableExt, Signal, WritableExt};
5use http::StatusCode;
6use http::{request::Parts, HeaderMap};
7use std::{cell::RefCell, rc::Rc};
8
9/// The context provided by dioxus fullstack for server-side rendering.
10///
11/// This context will only be set on the server during a streaming response.
12#[derive(Clone, Debug)]
13pub struct FullstackContext {
14    current_status: Signal<StreamingStatus>,
15    request_headers: Rc<RefCell<http::request::Parts>>,
16    response_headers: Rc<RefCell<Option<HeaderMap>>>,
17    route_http_status: Signal<HttpError>,
18}
19
20impl PartialEq for FullstackContext {
21    fn eq(&self, other: &Self) -> bool {
22        self.current_status == other.current_status
23            && Rc::ptr_eq(&self.request_headers, &other.request_headers)
24    }
25}
26
27impl FullstackContext {
28    /// Create a new streaming context. You should not need to call this directly. Dioxus fullstack will
29    /// provide this context for you.
30    pub fn new(parts: Parts) -> Self {
31        Self {
32            current_status: Signal::new(StreamingStatus::RenderingInitialChunk),
33            request_headers: Rc::new(RefCell::new(parts)),
34            route_http_status: Signal::new(HttpError {
35                status: http::StatusCode::OK,
36                message: None,
37            }),
38            response_headers: Rc::new(RefCell::new(Some(HeaderMap::new()))),
39        }
40    }
41
42    /// Commit the initial chunk of the response. This will be called automatically if you are using the
43    /// dioxus router when the suspense boundary above the router is resolved. Otherwise, you will need
44    /// to call this manually to start the streaming part of the response.
45    ///
46    /// Once this method has been called, the http response parts can no longer be modified.
47    pub fn commit_initial_chunk(&mut self) {
48        self.current_status
49            .set(StreamingStatus::InitialChunkCommitted);
50    }
51
52    /// Get the current status of the streaming response. This method is reactive and will cause
53    /// the current reactive context to rerun when the status changes.
54    pub fn streaming_state(&self) -> StreamingStatus {
55        *self.current_status.read()
56    }
57
58    /// Access the http request parts mutably. This will allow you to modify headers and other parts of the request.
59    pub fn parts_mut(&self) -> std::cell::RefMut<'_, http::request::Parts> {
60        self.request_headers.borrow_mut()
61    }
62
63    /// Extract an axum extractor from the current request. This will always use an empty body for the request,
64    /// since it's assumed that rendering the app is done under a `GET` request.
65    pub async fn extract<T: FromRequest<(), M>, M>() -> Result<T, ServerFnError> {
66        let this = Self::current()
67            .ok_or_else(|| ServerFnError::new("No FullstackContext found".to_string()))?;
68
69        let parts = this.request_headers.borrow_mut().clone();
70        let request =
71            axum_core::extract::Request::from_parts(parts, axum_core::body::Body::empty());
72        match T::from_request(request, &()).await {
73            Ok(res) => Ok(res),
74            Err(err) => {
75                let resp = err.into_response();
76                Err(ServerFnError::from_axum_response(resp).await)
77            }
78        }
79    }
80
81    /// Get the current `FullstackContext` if it exists. This will return `None` if called on the client
82    /// or outside of a streaming response on the server.
83    pub fn current() -> Option<Self> {
84        if let Some(rt) = dioxus_core::Runtime::try_current() {
85            let id = rt.try_current_scope_id()?;
86            if let Some(ctx) = rt.consume_context::<FullstackContext>(id) {
87                return Some(ctx);
88            }
89        }
90
91        None
92    }
93
94    /// Get the current HTTP status for the route. This will default to 200 OK, but can be modified
95    /// by calling `FullstackContext::commit_error_status` with an error.
96    pub fn current_http_status(&self) -> HttpError {
97        self.route_http_status.read().clone()
98    }
99
100    pub fn set_current_http_status(&mut self, status: HttpError) {
101        self.route_http_status.set(status);
102    }
103
104    /// Add a header to the response. This will be sent to the client when the response is committed.
105    pub fn add_response_header(
106        &self,
107        key: impl Into<http::header::HeaderName>,
108        value: impl Into<http::header::HeaderValue>,
109    ) {
110        if let Some(headers) = self.response_headers.borrow_mut().as_mut() {
111            headers.insert(key.into(), value.into());
112        }
113    }
114
115    /// Take the response headers out of the context. This will leave the context without any headers,
116    /// so it should only be called once when the response is being committed.
117    pub fn take_response_headers(&self) -> Option<HeaderMap> {
118        self.response_headers.borrow_mut().take()
119    }
120
121    /// Set the current HTTP status for the route. This will be used when committing the response
122    /// to the client.
123    pub fn commit_http_status(status: StatusCode, message: Option<String>) {
124        if let Some(mut ctx) = Self::current() {
125            ctx.set_current_http_status(HttpError { status, message });
126        }
127    }
128
129    /// Commit the CapturedError as the current HTTP status for the route.
130    /// This will attempt to downcast the error to known types and set the appropriate
131    /// status code. If the error type is unknown, it will default to
132    /// `StatusCode::INTERNAL_SERVER_ERROR`.
133    pub fn commit_error_status(error: impl Into<CapturedError>) -> HttpError {
134        let error = error.into();
135        let status = status_code_from_error(&error);
136        let http_error = HttpError {
137            status,
138            message: Some(error.to_string()),
139        };
140
141        if let Some(mut ctx) = Self::current() {
142            ctx.set_current_http_status(http_error.clone());
143        }
144
145        http_error
146    }
147}
148
149/// The status of the streaming response
150#[derive(Clone, Copy, Debug, PartialEq)]
151pub enum StreamingStatus {
152    /// The initial chunk is still being rendered. The http response parts can still be modified at this point.
153    RenderingInitialChunk,
154
155    /// The initial chunk has been committed and the response is now streaming. The http response parts
156    /// have already been sent to the client and can no longer be modified.
157    InitialChunkCommitted,
158}
159
160/// Commit the initial chunk of the response. This will be called automatically if you are using the
161/// dioxus router when the suspense boundary above the router is resolved. Otherwise, you will need
162/// to call this manually to start the streaming part of the response.
163///
164/// On the client, this will do nothing.
165///
166/// # Example
167/// ```rust, no_run
168/// # use dioxus::prelude::*;
169/// # use dioxus_fullstack_core::*;
170/// # fn Children() -> Element { unimplemented!() }
171/// fn App() -> Element {
172///     // This will start streaming immediately after the current render is complete.
173///     use_hook(commit_initial_chunk);
174///
175///     rsx! { Children {} }
176/// }
177/// ```
178pub fn commit_initial_chunk() {
179    crate::history::finalize_route();
180    if let Some(mut streaming) = try_consume_context::<FullstackContext>() {
181        streaming.commit_initial_chunk();
182    }
183}
184
185/// Get the current status of the streaming response. This method is reactive and will cause
186/// the current reactive context to rerun when the status changes.
187///
188/// On the client, this will always return `StreamingStatus::InitialChunkCommitted`.
189///
190/// # Example
191/// ```rust, no_run
192/// # use dioxus::prelude::*;
193/// # use dioxus_fullstack_core::*;
194/// #[component]
195/// fn MetaTitle(title: String) -> Element {
196///     // If streaming has already started, warn the user that the meta tag will not show
197///     // up in the initial chunk.
198///     use_hook(|| {
199///         if current_status() == StreamingStatus::InitialChunkCommitted {
200///            dioxus::logger::tracing::warn!("Since `MetaTitle` was rendered after the initial chunk was committed, the meta tag will not show up in the head without javascript enabled.");
201///         }
202///     });
203///
204///     rsx! { meta { property: "og:title", content: title } }
205/// }
206/// ```
207pub fn current_status() -> StreamingStatus {
208    if let Some(streaming) = try_consume_context::<FullstackContext>() {
209        streaming.streaming_state()
210    } else {
211        StreamingStatus::InitialChunkCommitted
212    }
213}
214
215/// Convert a `CapturedError` into an appropriate HTTP status code.
216///
217/// This will attempt to downcast the error to known types and return a corresponding status code.
218/// If the error type is unknown, it will default to `StatusCode::INTERNAL_SERVER_ERROR`.
219pub fn status_code_from_error(error: &CapturedError) -> StatusCode {
220    if let Some(err) = error.downcast_ref::<ServerFnError>() {
221        match err {
222            ServerFnError::ServerError { code, .. } => {
223                return StatusCode::from_u16(*code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
224            }
225            _ => return StatusCode::INTERNAL_SERVER_ERROR,
226        }
227    }
228
229    if let Some(err) = error.downcast_ref::<StatusCode>() {
230        return *err;
231    }
232
233    if let Some(err) = error.downcast_ref::<HttpError>() {
234        return err.status;
235    }
236
237    StatusCode::INTERNAL_SERVER_ERROR
238}