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}