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

+48 -71
View File
@@ -11,8 +11,7 @@ use std::sync::Arc;
use crate::IncrementalRendererConfig;
#[allow(unused)]
pub(crate) type ContextProviders =
Arc<Vec<Box<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync + 'static>>>;
pub(crate) type ContextProviders = Arc<Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync + 'static>>>;
/// A ServeConfig is used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus_ssr`].
#[derive(Clone, Default)]
@@ -21,8 +20,7 @@ pub struct ServeConfigBuilder {
pub(crate) index_html: Option<String>,
pub(crate) index_path: Option<PathBuf>,
pub(crate) incremental: Option<IncrementalRendererConfig>,
pub(crate) context_providers:
Vec<Arc<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync + 'static>>,
pub(crate) context_providers: Vec<Arc<dyn Fn() -> Box<dyn Any> + Send + Sync + 'static>>,
pub(crate) streaming_mode: StreamingMode,
}
@@ -130,31 +128,24 @@ impl ServeConfigBuilder {
/// use std::any::Any;
///
/// fn main() {
/// #[cfg(feature = "web")]
/// // Hydrate the application on the client
/// #[cfg(not(feature = "server"))]
/// dioxus::launch(app);
/// #[cfg(feature = "server")]
/// {
/// tokio::runtime::Runtime::new()
/// .unwrap()
/// .block_on(async move {
/// let address = dioxus_cli_config::fullstack_address_or_localhost();
/// let listener = tokio::net::TcpListener::bind(address)
/// .await
/// .unwrap();
/// let config = ServeConfigBuilder::default()
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
/// .context_providers(Arc::new(vec![Box::new(|| Box::new(1234u32) as Box<dyn Any>) as Box<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync>]));
/// axum::serve(
/// listener,
/// axum::Router::new()
/// .serve_dioxus_application(config, app)
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// });
/// }
///
/// #[cfg(feature = "server")]
/// dioxus_server::serve(|| async move {
/// use dioxus_server::{axum, ServeConfigBuilder, DioxusRouterExt};
///
/// let config = ServeConfigBuilder::default()
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
/// .context_providers(Arc::new(vec![Box::new(|| Box::new(1234u32) as Box<dyn Any>) as Box<dyn Fn() -> Box<dyn Any> + Send + Sync>]))
/// .build()?;
///
/// Ok(
/// axum::Router::new()
/// .serve_dioxus_application(config, app)
/// )
/// })
/// }
///
/// #[server]
@@ -195,31 +186,24 @@ impl ServeConfigBuilder {
/// use dioxus::prelude::*;
///
/// fn main() {
/// #[cfg(feature = "web")]
/// #[cfg(not(feature = "server"))]
/// // Hydrate the application on the client
/// dioxus::launch(app);
///
/// #[cfg(feature = "server")]
/// {
/// tokio::runtime::Runtime::new()
/// .unwrap()
/// .block_on(async move {
/// let address = dioxus_cli_config::fullstack_address_or_localhost();
/// let listener = tokio::net::TcpListener::bind(address)
/// .await
/// .unwrap();
/// let config = ServeConfigBuilder::default()
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
/// .context_provider(move || 1234u32);
/// axum::serve(
/// listener,
/// axum::Router::new()
/// .serve_dioxus_application(config, app)
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// });
/// }
/// dioxus_server::serve(|| async move {
/// use dioxus_server::{axum, ServeConfigBuilder, DioxusRouterExt};
///
/// let config = ServeConfigBuilder::default()
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
/// .context_provider(|| 1234u32)
/// .build()?;
///
/// Ok(
/// axum::Router::new()
/// .serve_dioxus_application(config, app)
/// )
/// });
/// }
///
/// #[server]
@@ -252,31 +236,24 @@ impl ServeConfigBuilder {
/// use dioxus::prelude::*;
///
/// fn main() {
/// #[cfg(feature = "web")]
/// // Hydrate the application on the client
/// #[cfg(not(feature = "server"))]
/// dioxus::launch(app);
///
/// // Run a custom server with axum on the server
/// #[cfg(feature = "server")]
/// {
/// tokio::runtime::Runtime::new()
/// .unwrap()
/// .block_on(async move {
/// let address = dioxus_cli_config::fullstack_address_or_localhost();
/// let listener = tokio::net::TcpListener::bind(address)
/// .await
/// .unwrap();
/// let config = ServeConfigBuilder::default()
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
/// .context(1234u32);
/// axum::serve(
/// listener,
/// axum::Router::new()
/// .serve_dioxus_application(config, app)
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// });
/// }
/// dioxus_server::serve(|| async move {
/// use dioxus_server::{axum, ServeConfigBuilder, DioxusRouterExt};
///
/// let config = ServeConfigBuilder::default()
/// .context(1234u32)
/// .build()?;
//
/// Ok(
/// axum::Router::new()
/// .serve_dioxus_application(config, app)
/// )
/// });
/// }
///
/// #[server]
+2
View File
@@ -15,6 +15,8 @@ pub(crate) use config::*;
pub use config::*;
pub use config::{ServeConfig, ServeConfigBuilder};
pub use document::ServerDocument;
pub use http;
pub use inventory;
pub use server::*;
pub mod redirect;
+10 -3
View File
@@ -12,6 +12,7 @@ use axum::{
routing::*,
};
use dioxus_core::{Element, VirtualDom};
use dioxus_fullstack_core::HttpError;
use futures::Stream;
use http::{header::*, request::Parts};
use std::path::Path;
@@ -299,10 +300,15 @@ impl RenderHandleState {
let (parts, _) = request.into_parts();
match state.render(parts, cfg, build_virtual_dom).await {
Ok((freshness, rx)) => {
let mut response =
axum::response::Html::from(Body::from_stream(rx)).into_response();
Ok((status, freshness, rx)) => {
let mut response = axum::response::Response::builder()
.status(status.status)
.header(CONTENT_TYPE, "text/html; charset=utf-8")
.body(Body::from_stream(rx))
.unwrap();
freshness.write(response.headers_mut());
Result::<http::Response<axum::body::Body>, StatusCode>::Ok(response)
}
@@ -338,6 +344,7 @@ impl RenderHandleState {
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
) -> Result<
(
HttpError,
RenderFreshness,
impl Stream<Item = Result<String, IncrementalRendererError>>,
),
+1 -15
View File
@@ -1,7 +1,7 @@
use axum::body::Body;
use axum::handler::Handler;
use axum::routing::MethodRouter;
use axum::Router; // both req/res // both req/res // req only
use axum::Router;
use dashmap::DashMap;
use dioxus_fullstack_core::DioxusServerState;
use http::Method;
@@ -111,20 +111,6 @@ impl ServerFunction {
// .map(|v| v.contains("text/html"))
// .unwrap_or(false);
// let mthd: MethodRouter<()> = axum::routing::get(self.handler);
// // this is taken from server_fn source...
// //
// // [`server_fn::axum::get_server_fn_service`]
// let mut service = {
// let middleware = self.middleware();
// let mut service = self.clone().boxed();
// for middleware in middleware {
// service = middleware.layer(service);
// }
// service
// };
// actually run the server fn (which may use the server context)
// let fut = crate::with_server_context(server_context.clone(), || service.run(req));
// let fut = crate::with_server_context(server_context.clone(), || service.run(req));
+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,