redirects, middleware, Transport, set/capture error status (#4751)

* wip: redirect

* redirect example

* fix file upload not in a form

* wip

* deny missing cfgs

* wip: add router fallback but it's not working

* fix suspense bug after rebuilds that queued work

* more cleanup

* add test for error codes

* add helpful comment

* bring back middleware annotation

* fix cfg

* add server feature

* fix doctest

* just disable liveview test for now

* fix liveview

* dont send init

* cache more playwright!

* add more pkgs to cache
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-10 21:43:57 -07:00
1 parent 8a81475ba8
commit 9243ef4128
35 files changed
+935 -800

No files matched your search

+41 -27
View File
@@ -11,8 +11,8 @@ use dioxus_core::{
SuspenseContext, TemplateNode, VNode, VirtualDom,
};
use dioxus_fullstack_core::{history::provide_fullstack_history_context, HttpError, ServerFnError};
use dioxus_fullstack_core::{FullstackContext, StreamingStatus};
use dioxus_fullstack_core::{HydrationContext, SerializedHydrationData};
use dioxus_fullstack_core::{StreamingContext, StreamingStatus};
use dioxus_router::ParseRouteError;
use dioxus_ssr::Renderer;
use futures_channel::mpsc::Sender;
@@ -67,30 +67,29 @@ impl SsrRendererPool {
route: &str,
render_into: &mut Sender<Result<String, IncrementalRendererError>>,
) -> Option<RenderFreshness> {
if let Some(incremental) = &self.incremental_cache {
if let Ok(mut incremental) = incremental.write() {
match incremental.get(route) {
Ok(Some(cached_render)) => {
let CachedRender {
freshness,
response,
..
} = cached_render;
_ = render_into.start_send(
String::from_utf8(response.to_vec())
.map_err(|err| IncrementalRendererError::Other(err.into())),
);
return Some(freshness);
}
Err(e) => {
tracing::error!(
"Failed to get route \"{route}\" from incremental cache: {e}"
);
}
_ => {}
let incremental = self.incremental_cache.as_ref()?;
if let Ok(mut incremental) = incremental.write() {
match incremental.get(route) {
Ok(Some(cached_render)) => {
let CachedRender {
freshness,
response,
..
} = cached_render;
_ = render_into.start_send(
String::from_utf8(response.to_vec())
.map_err(|err| IncrementalRendererError::Other(err.into())),
);
return Some(freshness);
}
Err(e) => {
tracing::error!("Failed to get route \"{route}\" from incremental cache: {e}");
}
_ => {}
}
}
None
}
@@ -103,6 +102,7 @@ impl SsrRendererPool {
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
) -> Result<
(
HttpError,
RenderFreshness,
impl Stream<Item = Result<String, IncrementalRendererError>>,
),
@@ -150,6 +150,10 @@ impl SsrRendererPool {
// before we even spawn anything, we can check synchronously if we have the route cached
if let Some(freshness) = self.check_cached_route(&route, &mut into) {
return Ok((
HttpError {
status: StatusCode::OK,
message: None,
},
freshness,
ReceiverWithDrop {
receiver: rx,
@@ -187,13 +191,14 @@ impl SsrRendererPool {
// Provide the document and streaming context to the root of the app
let streaming_context =
virtual_dom.in_scope(ScopeId::ROOT, || StreamingContext::new(parts));
virtual_dom.in_scope(ScopeId::ROOT, || FullstackContext::new(parts));
virtual_dom.provide_root_context(document.clone() as Rc<dyn dioxus_document::Document>);
virtual_dom.provide_root_context(streaming_context.clone());
virtual_dom.in_scope(ScopeId::ROOT, || {
// Wrap the memory history in a fullstack history provider to provide the initial route for hydration
provide_fullstack_history_context(history);
// Provide a hydration compatible error boundary that serializes errors for the client
dioxus_core::provide_create_error_boundary(
dioxus_fullstack_core::init_error_boundary,
@@ -203,6 +208,10 @@ impl SsrRendererPool {
// rebuild the virtual dom
virtual_dom.rebuild_in_place();
// Process any pending events that might have been triggered during the rebuild
// This ensures things like error boundaries get a chance to run.
virtual_dom.process_events();
// If streaming is disabled, wait for the virtual dom to finish all suspense work
// before rendering anything
if streaming_mode == StreamingMode::Disabled {
@@ -212,8 +221,9 @@ impl SsrRendererPool {
loop {
// Check if the router has finished and set the streaming context to finished
let streaming_context_finished = virtual_dom
.in_scope(ScopeId::ROOT, || streaming_context.current_status())
.in_scope(ScopeId::ROOT, || streaming_context.streaming_state())
== StreamingStatus::InitialChunkCommitted;
// Or if this app isn't using the router and has finished suspense
let suspense_finished = !virtual_dom.suspended_tasks_remaining();
if streaming_context_finished || suspense_finished {
@@ -228,7 +238,7 @@ impl SsrRendererPool {
}
}
// check if there are any errors
// check if there are any errors from the root error boundary
let error = virtual_dom.in_scope(ScopeId::ROOT_ERROR_BOUNDARY, || {
consume_context::<ErrorContext>().error()
});
@@ -287,8 +297,11 @@ impl SsrRendererPool {
return;
}
// Check the FullstackContext in case the user set the statuscode manually or via a layout.
let http_status = streaming_context.current_http_status();
// Now that we handled any errors from rendering, we can send the initial ok result
_ = initial_result_tx.send(Ok(()));
_ = initial_result_tx.send(Ok(http_status));
// Wait long enough to assemble the `<head>` of the document before starting to stream
let mut pre_body = String::new();
@@ -412,11 +425,12 @@ impl SsrRendererPool {
let join_handle = Self::spawn_platform(create_render_future);
// Wait for the initial result which determines the status code
initial_result_rx
let status = initial_result_rx
.await
.map_err(|err| SSRError::Incremental(IncrementalRendererError::Other(err.into())))??;
Ok((
status,
RenderFreshness::now(None),
ReceiverWithDrop {
receiver: rx,