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:
1 parent
8a81475ba8
commit
9243ef4128
35 files changed
+935
-800
No files matched your search
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user