Repository: DioxusLabs/dioxus
Pull request #3840: adds feature `axum_core` that compiles for wasm target exposing server functions and ssr
Author: login: conectado
Labels: name: enhancement
description: New feature or request
name: fullstack
description: related to the fullstack crate
Created: 2025-03-07T16:23:00Z | Merged: 2025-03-27T06:51:53Z

The current `axum` feature is incompatible with wasm32-unknown-unknown target due to depending on `tower-http` for serving static files.

With this patch I add the `axum_core` feature which disables `tower-http` functions and enables targeting wasm32-unknown-unknown with server functions enabled and ssr.

Note that for this to work, `server_fn` depends on `inventory` and as stated in this docs depending on the context you might need to expose and call [`__wasm_call_ctors`](https://docs.rs/inventory/latest/inventory/#webassembly-and-constructors) (I specifically needed that for Cloudflare workers)

Right now the feature isn't exposed by the main crate, I'm using it including the fullstack crate explicitly, I could add it to the main crate too!


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 1e96dbcad8..abdffe44e0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1322,7 +1322,6 @@ checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
 dependencies = [
  "async-trait",
  "axum-core 0.4.5",
- "axum-macros",
  "base64 0.22.1",
  "bytes",
  "futures-util",
@@ -1415,17 +1414,6 @@ dependencies = [
  "tower-service",
 ]
 
-[[package]]
-name = "axum-macros"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.98",
-]
-
 [[package]]
 name = "axum-server"
 version = "0.7.1"
@@ -3870,6 +3858,7 @@ name = "dioxus-examples"
 version = "0.6.3"
 dependencies = [
  "async-std",
+ "axum 0.7.9",
  "base64 0.22.1",
  "ciborium",
  "dioxus",
diff --git a/Cargo.toml b/Cargo.toml
index e6dd71b6d6..296bd99ecb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -191,7 +191,7 @@ tauri-utils = { version = "=2.1.1" }
 tauri-bundler = { version = "=2.2.3" }
 lru = "0.12.2"
 async-trait = "0.1.77"
-axum = "0.7.0"
+axum = { version = "0.7.0", default-features = false }
 axum-server = { version = "0.7.1", default-features = false }
 tower = "0.4.13"
 http = "1.0.0"
@@ -348,6 +348,7 @@ rand = { version = "0.8.4", features = ["small_rng"] }
 form_urlencoded = "1.2.0"
 async-std = "1.12.0"
 web-time = "1.1.0"
+axum = { workspace = true, default-features = true }
 
 [target.'cfg(target_arch = "wasm32")'.dev-dependencies]
 getrandom = { version = "0.2.12", features = ["js"] }
diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml
index 5549e53066..958bd9603b 100644
--- a/packages/cli/Cargo.toml
+++ b/packages/cli/Cargo.toml
@@ -53,7 +53,7 @@ krates = { version = "0.17.0" }
 cargo-config2 = { workspace = true, optional = true }
 regex = "1.10.6"
 
-axum = { workspace = true, features = ["ws"] }
+axum = { workspace = true, default-features = true, features = ["ws"] }
 axum-server = { workspace = true, features = ["tls-rustls"] }
 axum-extra = { workspace = true, features = ["typed-header"] }
 tower-http = { workspace = true, features = ["full"] }
diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml
index 803757c684..d63f50f8fb 100644
--- a/packages/fullstack/Cargo.toml
+++ b/packages/fullstack/Cargo.toml
@@ -16,7 +16,7 @@ server_fn = { version = "0.7.3", features = ["json", "url", "browser"], default-
 dioxus_server_macro = { workspace = true }
 
 # axum
-axum = { workspace = true, features = ["ws", "macros"], optional = true }
+axum = { workspace = true, optional = true }
 tower-http = { workspace = true, optional = true, features = ["fs"] }
 
 dioxus-lib = { workspace = true }
@@ -88,7 +88,18 @@ desktop = ["dep:dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwe
 mobile = ["dep:dioxus-mobile", "server_fn/reqwest", "dioxus_server_macro/reqwest"]
 default-tls = ["server_fn/default-tls"]
 rustls = ["server_fn/rustls", "dep:rustls", "dep:hyper-rustls"]
-axum = ["dep:axum", "dep:tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum", "default-tls"]
+axum_core = [
+    "dep:axum",
+    "server_fn/axum-no-default",
+    "dioxus_server_macro/axum",
+    "default-tls",
+    "server",
+]
+axum = [
+    "dep:tower-http",
+    "server_fn/axum",
+    "axum_core",
+]
 server = [
     "server_fn/ssr",
     "dioxus_server_macro/server",
diff --git a/packages/fullstack/src/axum_core.rs b/packages/fullstack/src/axum_core.rs
new file mode 100644
index 0000000000..162b6ada1d
--- /dev/null
+++ b/packages/fullstack/src/axum_core.rs
@@ -0,0 +1,393 @@
+//! Dioxus core utilities for the [Axum](https://docs.rs/axum/latest/axum/index.html) server framework.
+//!
+//! # Example
+//! ```rust, no_run
+//! #![allow(non_snake_case)]
+//! use dioxus::prelude::*;
+//!
+//! fn main() {
+//!     #[cfg(feature = "web")]
+//!     // Hydrate the application on the client
+//!     dioxus::launch(app);
+//!     #[cfg(feature = "server")]
+//!     {
+//!         tokio::runtime::Runtime::new()
+//!             .unwrap()
+//!             .block_on(async move {
+//!                 // Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address
+//!                 // and we use the generated address the CLI gives us
+//!                 let address = dioxus::cli_config::fullstack_address_or_localhost();
+//!                 let listener = tokio::net::TcpListener::bind(address)
+//!                     .await
+//!                     .unwrap();
+//!                 axum::serve(
+//!                         listener,
+//!                         axum::Router::new()
+//!                             // Server side render the application, serve static assets, and register server functions
+//!                             .register_server_functions()
+//!                             .fallback(get(render_handler)
+//!                                 // Note: ServeConfig::new won't work on WASM
+//!                                 .with_state(RenderHandler::new(ServeConfig::new().unwrap(), app))
+//!                             )
+//!                             .into_make_service(),
+//!                     )
+//!                     .await
+//!                     .unwrap();
+//!             });
+//!      }
+//! }
+//!
+//! fn app() -> Element {
+//!     let mut text = use_signal(|| "...".to_string());
+//!
+//!     rsx! {
+//!         button {
+//!             onclick: move |_| async move {
+//!                 if let Ok(data) = get_server_data().await {
+//!                     text.set(data);
+//!                 }
+//!             },
+//!             "Run a server function"
+//!         }
+//!         "Server said: {text}"
+//!     }
+//! }
+//!
+//! #[server(GetServerData)]
+//! async fn get_server_data() -> Result<String, ServerFnError> {
+//!     Ok("Hello from the server!".to_string())
+//! }
+//!
+//! # WASM support
+//!
+//! These utilities compile to the WASM family of targets, while the more complete ones found in [server] don't
+//! ```
+
+use std::sync::Arc;
+
+use crate::prelude::*;
+use crate::render::SSRError;
+use crate::ContextProviders;
+
+use axum::body;
+use axum::extract::State;
+use axum::routing::*;
+use axum::{
+    body::Body,
+    http::{Request, Response, StatusCode},
+    response::IntoResponse,
+};
+use dioxus_lib::prelude::{Element, VirtualDom};
+use http::header::*;
+
+/// A extension trait with server function utilities for integrating Dioxus with your Axum router.
+pub trait DioxusRouterFnExt<S> {
+    /// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
+    ///
+    /// # Example
+    /// ```rust, no_run
+    /// # use dioxus_lib::prelude::*;
+    /// # use dioxus_fullstack::prelude::*;
+    /// #[tokio::main]
+    /// async fn main() {
+    ///     let addr = dioxus::cli_config::fullstack_address_or_localhost();
+    ///     let router = axum::Router::new()
+    ///         // Register server functions routes with the default handler
+    ///         .register_server_functions()
+    ///         .into_make_service();
+    ///     let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
+    ///     axum::serve(listener, router).await.unwrap();
+    /// }
+    /// ```
+    #[allow(dead_code)]
+    fn register_server_functions(self) -> Self
+    where
+        Self: Sized,
+    {
+        self.register_server_functions_with_context(Default::default())
+    }
+
+    /// Registers server functions with some additional context to insert into the [`DioxusServerContext`] for that handler.
+    ///
+    /// # Example
+    /// ```rust, no_run
+    /// # use dioxus_lib::prelude::*;
+    /// # use dioxus_fullstack::prelude::*;
+    /// # use std::sync::Arc;
+    /// #[tokio::main]
+    /// async fn main() {
+    ///     let addr = dioxus::cli_config::fullstack_address_or_localhost();
+    ///     let router = axum::Router::new()
+    ///         // Register server functions routes with the default handler
+    ///         .register_server_functions_with_context(Arc::new(vec![Box::new(|| Box::new(1234567890u32))]))
+    ///         .into_make_service();
+    ///     let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
+    ///     axum::serve(listener, router).await.unwrap();
+    /// }
+    /// ```
+    fn register_server_functions_with_context(self, context_providers: ContextProviders) -> Self;
+}
+
+impl<S> DioxusRouterFnExt<S> for Router<S>
+where
+    S: Send + Sync + Clone + 'static,
+{
+    fn register_server_functions_with_context(
+        mut self,
+        context_providers: ContextProviders,
+    ) -> Self {
+        use http::method::Method;
+
+        for (path, method) in server_fn::axum::server_fn_paths() {
+            tracing::trace!("Registering server function: {} {}", method, path);
+            let context_providers = context_providers.clone();
+            let handler = move |req| handle_server_fns_inner(path, context_providers, req);
+            self = match method {
+                Method::GET => self.route(path, get(handler)),
+                Method::POST => self.route(path, post(handler)),
+                Method::PUT => self.route(path, put(handler)),
+                _ => unimplemented!("Unsupported server function method: {}", method),
+            };
+        }
+
+        self
+    }
+}
+
+/// A handler for Dioxus server functions. This will run the server function and return the result.
+async fn handle_server_fns_inner(
+    path: &str,
+    additional_context: ContextProviders,
+    req: Request<Body>,
+) -> impl IntoResponse {
+    use server_fn::middleware::Service;
+
+    let path_string = path.to_string();
+
+    let (parts, body) = req.into_parts();
+    let req = Request::from_parts(parts.clone(), body);
+    let method = req.method().clone();
+
+    if let Some(mut service) =
+            server_fn::axum::get_server_fn_service(&path_string, method)
+        {
+            // Create the server context with info from the request
+            let server_context = DioxusServerContext::new(parts);
+            // Provide additional context from the render state
+            add_server_context(&server_context, &additional_context);
+
+            // store Accepts and Referrer in case we need them for redirect (below)
+            let accepts_html = req
+                .headers()
+                .get(ACCEPT)
+                .and_then(|v| v.to_str().ok())
+                .map(|v| v.contains("text/html"))
+                .unwrap_or(false);
+            let referrer = req.headers().get(REFERER).cloned();
+
+            // actually run the server fn (which may use the server context)
+            let fut = with_server_context(server_context.clone(), || service.run(req));
+            let mut res = ProvideServerContext::new(fut, server_context.clone()).await;
+
+            // it it accepts text/html (i.e., is a plain form post) and doesn't already have a
+            // Location set, then redirect to Referer
+            if accepts_html {
+                if let Some(referrer) = referrer {
+                    let has_location = res.headers().get(LOCATION).is_some();
+                    if !has_location {
+                        *res.status_mut() = StatusCode::FOUND;
+                        res.headers_mut().insert(LOCATION, referrer);
+                    }
+                }
+            }
+
+            // apply the response parts from the server context to the response
+            let mut res_options = server_context.response_parts_mut();
+            res.headers_mut().extend(res_options.headers.drain());
+
+            Ok(res)
+        } else {
+            Response::builder().status(StatusCode::BAD_REQUEST).body(
+                {
+                    #[cfg(target_family = "wasm")]
+                    {
+                        Body::from(format!(
+                            "No server function found for path: {path_string}\nYou may need to explicitly register the server function with `register_explicit`, rebuild your wasm binary to update a server function link or make sure the prefix your server and client use for server functions match.",
+                        ))
+                    }
+                    #[cfg(not(target_family = "wasm"))]
+                    {
+                        Body::from(format!(
+                            "No server function found for path: {path_string}\nYou may need to rebuild your wasm binary to update a server function link or make sure the prefix your server and client use for server functions match.",
+                        ))
+                    }
+                }
+            )
+        }
+        .expect("could not build Response")
+}
+
+pub(crate) fn add_server_context(
+    server_context: &DioxusServerContext,
+    context_providers: &ContextProviders,
+) {
+    for index in 0..context_providers.len() {
+        let context_providers = context_providers.clone();
+        server_context.insert_boxed_factory(Box::new(move || context_providers[index]()));
+    }
+}
+
+fn apply_request_parts_to_response<B>(
+    headers: hyper::header::HeaderMap,
+    response: &mut axum::response::Response<B>,
+) {
+    let mut_headers = response.headers_mut();
+    for (key, value) in headers.iter() {
+        mut_headers.insert(key, value.clone());
+    }
+}
+
+/// State used by [`render_handler`] to render a dioxus component with axum
+#[derive(Clone)]
+pub struct RenderHandleState {
+    config: ServeConfig,
+    build_virtual_dom: Arc<dyn Fn() -> VirtualDom + Send + Sync>,
+    ssr_state: once_cell::sync::OnceCell<SSRState>,
+}
+
+impl RenderHandleState {
+    /// Create a new [`RenderHandleState`]
+    pub fn new(config: ServeConfig, root: fn() -> Element) -> Self {
+        Self {
+            config,
+            build_virtual_dom: Arc::new(move || VirtualDom::new(root)),
+            ssr_state: Default::default(),
+        }
+    }
+
+    /// Create a new [`RenderHandleState`] with a custom [`VirtualDom`] factory. This method can be used to pass context into the root component of your application.
+    pub fn new_with_virtual_dom_factory(
+        config: ServeConfig,
+        build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
+    ) -> Self {
+        Self {
+            config,
+            build_virtual_dom: Arc::new(build_virtual_dom),
+            ssr_state: Default::default(),
+        }
+    }
+
+    /// Set the [`ServeConfig`] for this [`RenderHandleState`]
+    pub fn with_config(mut self, config: ServeConfig) -> Self {
+        self.config = config;
+        self
+    }
+
+    /// Set the [`SSRState`] for this [`RenderHandleState`]. Sharing a [`SSRState`] between multiple [`RenderHandleState`]s is more efficient than creating a new [`SSRState`] for each [`RenderHandleState`].
+    pub fn with_ssr_state(mut self, ssr_state: SSRState) -> Self {
+        self.ssr_state = once_cell::sync::OnceCell::new();
+        if self.ssr_state.set(ssr_state).is_err() {
+            panic!("SSRState already set");
+        }
+        self
+    }
+
+    fn ssr_state(&self) -> &SSRState {
+        self.ssr_state.get_or_init(|| SSRState::new(&self.config))
+    }
+}
+
+/// SSR renderer handler for Axum with added context injection.
+///
+/// # Example
+/// ```rust,no_run
+/// #![allow(non_snake_case)]
+/// use std::sync::{Arc, Mutex};
+///
+/// use axum::routing::get;
+/// use dioxus::prelude::*;
+///
+/// fn app() -> Element {
+///     rsx! {
+///         "hello!"
+///     }
+/// }
+///
+/// #[tokio::main]
+/// async fn main() {
+///     let addr = dioxus::cli_config::fullstack_address_or_localhost();
+///     let router = axum::Router::new()
+///         // Register server functions, etc.
+///         // Note you can use `register_server_functions_with_context`
+///         // to inject the context into server functions running outside
+///         // of an SSR render context.
+///         .fallback(get(render_handler)
+///             .with_state(RenderHandleState::new(ServeConfig::new().unwrap(), app))
+///         )
+///         .into_make_service();
+///     let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
+///     axum::serve(listener, router).await.unwrap();
+/// }
+/// ```
+pub async fn render_handler(
+    State(state): State<RenderHandleState>,
+    request: Request<Body>,
+) -> impl IntoResponse {
+    let cfg = &state.config;
+    let ssr_state = state.ssr_state();
+    let build_virtual_dom = {
+        let build_virtual_dom = state.build_virtual_dom.clone();
+        let context_providers = state.config.context_providers.clone();
+        move || {
+            let mut vdom = build_virtual_dom();
+            for state in context_providers.as_slice() {
+                vdom.insert_any_root_context(state());
+            }
+            vdom
+        }
+    };
+
+    let (parts, _) = request.into_parts();
+    let url = parts
+        .uri
+        .path_and_query()
+        .ok_or(StatusCode::BAD_REQUEST)?
+        .to_string();
+    let parts: Arc<parking_lot::RwLock<http::request::Parts>> =
+        Arc::new(parking_lot::RwLock::new(parts));
+    // Create the server context with info from the request
+    let server_context = DioxusServerContext::from_shared_parts(parts.clone());
+    // Provide additional context from the render state
+    add_server_context(&server_context, &state.config.context_providers);
+
+    match ssr_state
+        .render(url, cfg, build_virtual_dom, &server_context)
+        .await
+    {
+        Ok((freshness, rx)) => {
+            let mut response = axum::response::Html::from(Body::from_stream(rx)).into_response();
+            freshness.write(response.headers_mut());
+            let headers = server_context.response_parts().headers.clone();
+            apply_request_parts_to_response(headers, &mut response);
+            Result::<http::Response<axum::body::Body>, StatusCode>::Ok(response)
+        }
+        Err(SSRError::Incremental(e)) => {
+            tracing::error!("Failed to render page: {}", e);
+            Ok(report_err(e).into_response())
+        }
+        Err(SSRError::Routing(e)) => {
+            tracing::trace!("Page not found: {}", e);
+            Ok(Response::builder()
+                .status(StatusCode::NOT_FOUND)
+                .body(Body::from("Page not found"))
+                .unwrap())
+        }
+    }
+}
+
+fn report_err<E: std::fmt::Display>(e: E) -> Response<axum::body::Body> {
+    Response::builder()
+        .status(StatusCode::INTERNAL_SERVER_ERROR)
+        .body(body::Body::new(format!("Error: {}", e)))
+        .unwrap()
+}
diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs
index b7cf7dc43a..975d4fcd05 100644
--- a/packages/fullstack/src/lib.rs
+++ b/packages/fullstack/src/lib.rs
@@ -4,14 +4,24 @@
 #![deny(missing_docs)]
 #![cfg_attr(docsrs, feature(doc_cfg))]
 
+use std::sync::Arc;
+
 pub use once_cell;
 
 mod html_storage;
 
+#[allow(unused)]
+pub(crate) type ContextProviders =
+    Arc<Vec<Box<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync + 'static>>>;
+
 #[cfg(feature = "axum")]
 #[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
 pub mod server;
 
+#[cfg(feature = "axum_core")]
+#[cfg_attr(docsrs, doc(cfg(feature = "axum_core")))]
+pub mod axum_core;
+
 mod hooks;
 
 pub mod document;
@@ -22,6 +32,7 @@ mod streaming;
 
 #[cfg(feature = "server")]
 mod serve_config;
+
 #[cfg(feature = "server")]
 pub use serve_config::*;
 
@@ -37,6 +48,9 @@ pub mod prelude {
     #[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
     pub use crate::server::*;
 
+    #[cfg(feature = "axum_core")]
+    pub use crate::axum_core::*;
+
     #[cfg(feature = "server")]
     #[cfg_attr(docsrs, doc(cfg(feature = "server")))]
     pub use crate::render::{FullstackHTMLTemplate, SSRState};
@@ -45,8 +59,8 @@ pub mod prelude {
     #[cfg_attr(docsrs, doc(cfg(feature = "server")))]
     pub use crate::serve_config::{ServeConfig, ServeConfigBuilder};
 
-    #[cfg(all(feature = "server", feature = "axum"))]
-    #[cfg_attr(docsrs, doc(cfg(all(feature = "server", feature = "axum"))))]
+    #[cfg(feature = "axum")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
     pub use crate::server_context::Axum;
 
     #[cfg(feature = "server")]
diff --git a/packages/fullstack/src/serve_config.rs b/packages/fullstack/src/serve_config.rs
index 46642eaca6..0bf0757325 100644
--- a/packages/fullstack/src/serve_config.rs
+++ b/packages/fullstack/src/serve_config.rs
@@ -8,8 +8,7 @@ use std::io::Read;
 use std::path::PathBuf;
 use std::sync::Arc;
 
-pub(crate) type ContextProviders =
-    Arc<Vec<Box<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync + 'static>>>;
+use crate::ContextProviders;
 
 /// 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)]
@@ -337,20 +336,24 @@ impl ServeConfigBuilder {
     }
 
     /// Build the ServeConfig. This may fail if the index.html file is not found.
+    ///
+    /// ## WASM compatibility
+    /// In the context of WASM the file system normally can't be read so this always requires
+    /// the `index_html` field to be set.
     pub fn build(self) -> Result<ServeConfig, UnableToLoadIndex> {
-        // The CLI always bundles static assets into the exe/public directory
-        let public_path = public_path();
-
-        let index_path = self
-            .index_path
-            .map(PathBuf::from)
-            .unwrap_or_else(|| public_path.join("index.html"));
-
         let root_id = self.root_id.unwrap_or("main");
 
         let index_html = match self.index_html {
             Some(index) => index,
-            None => load_index_path(index_path)?,
+            None => {
+                // The CLI always bundles static assets into the exe/public directory
+                let public_path = public_path();
+
+                let index_path = self
+                    .index_path
+                    .unwrap_or_else(|| public_path.join("index.html"));
+                load_index_path(index_path)?
+            }
         };
 
         let index = load_index_html(index_html, root_id);
@@ -491,6 +494,7 @@ impl LaunchConfig for ServeConfig {}
 
 impl ServeConfig {
     /// Create a new ServeConfig
+    #[cfg(not(target_family = "wasm"))]
     pub fn new() -> Result<Self, UnableToLoadIndex> {
         ServeConfigBuilder::new().build()
     }
diff --git a/packages/fullstack/src/server/mod.rs b/packages/fullstack/src/server/mod.rs
index fe725d45be..cfe8bb00de 100644
--- a/packages/fullstack/src/server/mod.rs
+++ b/packages/fullstack/src/server/mod.rs
@@ -58,66 +58,12 @@
 pub mod launch;
 
 use axum::routing::*;
-use axum::{
-    body::{self, Body},
-    extract::State,
-    http::{Request, Response, StatusCode},
-    response::IntoResponse,
-};
-use dioxus_lib::prelude::{Element, VirtualDom};
-use http::header::*;
+use dioxus_lib::prelude::Element;
 
-use std::sync::Arc;
-
-use crate::render::SSRError;
-use crate::{prelude::*, ContextProviders};
+use crate::prelude::*;
 
 /// A extension trait with utilities for integrating Dioxus with your Axum router.
-pub trait DioxusRouterExt<S> {
-    /// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
-    ///
-    /// # Example
-    /// ```rust, no_run
-    /// # use dioxus_lib::prelude::*;
-    /// # use dioxus_fullstack::prelude::*;
-    /// #[tokio::main]
-    /// async fn main() {
-    ///     let addr = dioxus::cli_config::fullstack_address_or_localhost();
-    ///     let router = axum::Router::new()
-    ///         // Register server functions routes with the default handler
-    ///         .register_server_functions()
-    ///         .into_make_service();
-    ///     let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
-    ///     axum::serve(listener, router).await.unwrap();
-    /// }
-    /// ```
-    fn register_server_functions(self) -> Self
-    where
-        Self: Sized,
-    {
-        self.register_server_functions_with_context(Default::default())
-    }
-
-    /// Registers server functions with some additional context to insert into the [`DioxusServerContext`] for that handler.
-    ///
-    /// # Example
-    /// ```rust, no_run
-    /// # use dioxus_lib::prelude::*;
-    /// # use dioxus_fullstack::prelude::*;
-    /// # use std::sync::Arc;
-    /// #[tokio::main]
-    /// async fn main() {
-    ///     let addr = dioxus::cli_config::fullstack_address_or_localhost();
-    ///     let router = axum::Router::new()
-    ///         // Register server functions routes with the default handler
-    ///         .register_server_functions_with_context(Arc::new(vec![Box::new(|| Box::new(1234567890u32))]))
-    ///         .into_make_service();
-    ///     let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
-    ///     axum::serve(listener, router).await.unwrap();
-    /// }
-    /// ```
-    fn register_server_functions_with_context(self, context_providers: ContextProviders) -> Self;
-
+pub trait DioxusRouterExt<S>: DioxusRouterFnExt<S> {
     /// Serves the static WASM for your Dioxus application (except the generated index.html).
     ///
     /// # Example
@@ -176,27 +122,6 @@ impl<S> DioxusRouterExt<S> for Router<S>
 where
     S: Send + Sync + Clone + 'static,
 {
-    fn register_server_functions_with_context(
-        mut self,
-        context_providers: ContextProviders,
-    ) -> Self {
-        use http::method::Method;
-
-        for (path, method) in server_fn::axum::server_fn_paths() {
-            tracing::trace!("Registering server function: {} {}", method, path);
-            let context_providers = context_providers.clone();
-            let handler = move |req| handle_server_fns_inner(path, context_providers, req);
-            self = match method {
-                Method::GET => self.route(path, get(handler)),
-                Method::POST => self.route(path, post(handler)),
-                Method::PUT => self.route(path, put(handler)),
-                _ => unimplemented!("Unsupported server function method: {}", method),
-            };
-        }
-
-        self
-    }
-
     fn serve_static_assets(mut self) -> Self {
         use tower_http::services::{ServeDir, ServeFile};
 
@@ -272,262 +197,3 @@ where
         }
     }
 }
-
-fn apply_request_parts_to_response<B>(
-    headers: hyper::header::HeaderMap,
-    response: &mut axum::response::Response<B>,
-) {
-    let mut_headers = response.headers_mut();
-    for (key, value) in headers.iter() {
-        mut_headers.insert(key, value.clone());
-    }
-}
-
-fn add_server_context(server_context: &DioxusServerContext, context_providers: &ContextProviders) {
-    for index in 0..context_providers.len() {
-        let context_providers = context_providers.clone();
-        server_context.insert_boxed_factory(Box::new(move || context_providers[index]()));
-    }
-}
-
-/// State used by [`render_handler`] to render a dioxus component with axum
-#[derive(Clone)]
-pub struct RenderHandleState {
-    config: ServeConfig,
-    build_virtual_dom: Arc<dyn Fn() -> VirtualDom + Send + Sync>,
-    ssr_state: once_cell::sync::OnceCell<SSRState>,
-}
-
-impl RenderHandleState {
-    /// Create a new [`RenderHandleState`]
-    pub fn new(config: ServeConfig, root: fn() -> Element) -> Self {
-        Self {
-            config,
-            build_virtual_dom: Arc::new(move || VirtualDom::new(root)),
-            ssr_state: Default::default(),
-        }
-    }
-
-    /// Create a new [`RenderHandleState`] with a custom [`VirtualDom`] factory. This method can be used to pass context into the root component of your application.
-    pub fn new_with_virtual_dom_factory(
-        config: ServeConfig,
-        build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
-    ) -> Self {
-        Self {
-            config,
-            build_virtual_dom: Arc::new(build_virtual_dom),
-            ssr_state: Default::default(),
-        }
-    }
-
-    /// Set the [`ServeConfig`] for this [`RenderHandleState`]
-    pub fn with_config(mut self, config: ServeConfig) -> Self {
-        self.config = config;
-        self
-    }
-
-    /// Set the [`SSRState`] for this [`RenderHandleState`]. Sharing a [`SSRState`] between multiple [`RenderHandleState`]s is more efficient than creating a new [`SSRState`] for each [`RenderHandleState`].
-    pub fn with_ssr_state(mut self, ssr_state: SSRState) -> Self {
-        self.ssr_state = once_cell::sync::OnceCell::new();
-        if self.ssr_state.set(ssr_state).is_err() {
-            panic!("SSRState already set");
-        }
-        self
-    }
-
-    fn ssr_state(&self) -> &SSRState {
-        self.ssr_state.get_or_init(|| SSRState::new(&self.config))
-    }
-}
-
-/// SSR renderer handler for Axum with added context injection.
-///
-/// # Example
-/// ```rust,no_run
-/// #![allow(non_snake_case)]
-/// use std::sync::{Arc, Mutex};
-///
-/// use axum::routing::get;
-/// use dioxus::prelude::*;
-///
-/// fn app() -> Element {
-///     rsx! {
-///         "hello!"
-///     }
-/// }
-///
-/// #[tokio::main]
-/// async fn main() {
-///     let addr = dioxus::cli_config::fullstack_address_or_localhost();
-///     let router = axum::Router::new()
-///         // Register server functions, etc.
-///         // Note you can use `register_server_functions_with_context`
-///         // to inject the context into server functions running outside
-///         // of an SSR render context.
-///         .fallback(get(render_handler)
-///             .with_state(RenderHandleState::new(ServeConfig::new().unwrap(), app))
-///         )
-///         .into_make_service();
-///     let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
-///     axum::serve(listener, router).await.unwrap();
-/// }
-/// ```
-pub async fn render_handler(
-    State(state): State<RenderHandleState>,
-    request: Request<Body>,
-) -> impl IntoResponse {
-    let cfg = &state.config;
-    let ssr_state = state.ssr_state();
-    let build_virtual_dom = {
-        let build_virtual_dom = state.build_virtual_dom.clone();
-        let context_providers = state.config.context_providers.clone();
-        move || {
-            let mut vdom = build_virtual_dom();
-            for state in context_providers.as_slice() {
-                vdom.insert_any_root_context(state());
-            }
-            vdom
-        }
-    };
-
-    let (parts, _) = request.into_parts();
-    let url = parts
-        .uri
-        .path_and_query()
-        .ok_or(StatusCode::BAD_REQUEST)?
-        .to_string();
-    let parts: Arc<parking_lot::RwLock<http::request::Parts>> =
-        Arc::new(parking_lot::RwLock::new(parts));
-    // Create the server context with info from the request
-    let server_context = DioxusServerContext::from_shared_parts(parts.clone());
-    // Provide additional context from the render state
-    add_server_context(&server_context, &state.config.context_providers);
-
-    match ssr_state
-        .render(url, cfg, build_virtual_dom, &server_context)
-        .await
-    {
-        Ok((freshness, rx)) => {
-            let mut response = axum::response::Html::from(Body::from_stream(rx)).into_response();
-            freshness.write(response.headers_mut());
-            let headers = server_context.response_parts().headers.clone();
-            apply_request_parts_to_response(headers, &mut response);
-            Result::<http::Response<axum::body::Body>, StatusCode>::Ok(response)
-        }
-        Err(SSRError::Incremental(e)) => {
-            tracing::error!("Failed to render page: {}", e);
-            Ok(report_err(e).into_response())
-        }
-        Err(SSRError::Routing(e)) => {
-            tracing::trace!("Page not found: {}", e);
-            Ok(Response::builder()
-                .status(StatusCode::NOT_FOUND)
-                .body(Body::from("Page not found"))
-                .unwrap())
-        }
-    }
-}
-
-fn report_err<E: std::fmt::Display>(e: E) -> Response<axum::body::Body> {
-    Response::builder()
-        .status(StatusCode::INTERNAL_SERVER_ERROR)
-        .body(body::Body::new(format!("Error: {}", e)))
-        .unwrap()
-}
-
-/// A handler for Dioxus server functions. This will run the server function and return the result.
-async fn handle_server_fns_inner(
-    path: &str,
-    additional_context: ContextProviders,
-    req: Request<Body>,
-) -> impl IntoResponse {
-    use server_fn::middleware::Service;
-
-    let path_string = path.to_string();
-
-    let future = move || async move {
-        let (parts, body) = req.into_parts();
-        let req = Request::from_parts(parts.clone(), body);
-        let method = req.method().clone();
-
-        if let Some(mut service) =
-            server_fn::axum::get_server_fn_service(&path_string, method)
-        {
-            // Create the server context with info from the request
-            let server_context = DioxusServerContext::new(parts);
-            // Provide additional context from the render state
-            add_server_context(&server_context, &additional_context);
-
-            // store Accepts and Referrer in case we need them for redirect (below)
-            let accepts_html = req
-                .headers()
-                .get(ACCEPT)
-                .and_then(|v| v.to_str().ok())
-                .map(|v| v.contains("text/html"))
-                .unwrap_or(false);
-            let referrer = req.headers().get(REFERER).cloned();
-
-            // actually run the server fn (which may use the server context)
-            let fut = with_server_context(server_context.clone(), || service.run(req));
-            let mut res = ProvideServerContext::new(fut, server_context.clone()).await;
-
-            // it it accepts text/html (i.e., is a plain form post) and doesn't already have a
-            // Location set, then redirect to Referer
-            if accepts_html {
-                if let Some(referrer) = referrer {
-                    let has_location = res.headers().get(LOCATION).is_some();
-                    if !has_location {
-                        *res.status_mut() = StatusCode::FOUND;
-                        res.headers_mut().insert(LOCATION, referrer);
-                    }
-                }
-            }
-
-            // apply the response parts from the server context to the response
-            let mut res_options = server_context.response_parts_mut();
-            res.headers_mut().extend(res_options.headers.drain());
-
-            Ok(res)
-        } else {
-            Response::builder().status(StatusCode::BAD_REQUEST).body(
-                {
-                    #[cfg(target_family = "wasm")]
-                    {
-                        Body::from(format!(
-                            "No server function found for path: {path_string}\nYou may need to explicitly register the server function with `register_explicit`, rebuild your wasm binary to update a server function link or make sure the prefix your server and client use for server functions match.",
-                        ))
-                    }
-                    #[cfg(not(target_family = "wasm"))]
-                    {
-                        Body::from(format!(
-                            "No server function found for path: {path_string}\nYou may need to rebuild your wasm binary to update a server function link or make sure the prefix your server and client use for server functions match.",
-                        ))
-                    }
-                }
-            )
-        }
-        .expect("could not build Response")
-    };
-    #[cfg(target_arch = "wasm32")]
-    {
-        use futures_util::future::FutureExt;
-
-        let result = tokio::task::spawn_local(future);
-        let result = result.then(|f| async move { f.unwrap() });
-        result.await.unwrap_or_else(|e| {
-            use server_fn::error::NoCustomError;
-            use server_fn::error::ServerFnErrorSerde;
-            (
-                StatusCode::INTERNAL_SERVER_ERROR,
-                ServerFnError::<NoCustomError>::ServerError(e.to_string())
-                    .ser()
-                    .unwrap_or_default(),
-            )
-                .into_response()
-        })
-    }
-    #[cfg(not(target_arch = "wasm32"))]
-    {
-        future().await
-    }
-}
diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml
index f0d1f80c2f..6b937fecb9 100644
--- a/packages/liveview/Cargo.toml
+++ b/packages/liveview/Cargo.toml
@@ -33,7 +33,7 @@ dioxus-cli-config = { workspace = true }
 generational-box = { workspace = true }
 
 # axum
-axum = { workspace = true, optional = true, features = ["ws"] }
+axum = { workspace = true, optional = true, default-features = true, features = ["ws"] }
 
 [dev-dependencies]
 tokio = { workspace = true, features = ["full"] }
diff --git a/packages/playwright-tests/liveview/Cargo.toml b/packages/playwright-tests/liveview/Cargo.toml
index 4213f738b5..3f25bafe7d 100644
--- a/packages/playwright-tests/liveview/Cargo.toml
+++ b/packages/playwright-tests/liveview/Cargo.toml
@@ -10,4 +10,4 @@ publish = false
 dioxus = { workspace = true }
 dioxus-liveview = { workspace = true, features = ["axum"] }
 tokio = { workspace = true, features = ["full"] }
-axum = { workspace = true, features = ["ws"] }
+axum = { workspace = true, default-features = true, features = ["ws"] }


Reviews:
user: login: ealmloff
body: Thanks for working on this! Re-exporting a different trait based on the feature flag can issues because [features need to be additive](https://doc.rust-lang.org/cargo/reference/features.html#feature-unification). If we make the main axum extension trait a super trait of the wasm compatible axum trait it should fix that issue. The SSR rendering code should also be wasm compatible already so that could be included in the axum wasm feature flag. The main thing that is not compatible with wasm is serving the static files with tower-http. If we move that to a separate trait, the rest of the integration should work in wasm
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-07T18:21:44Z
user: login: conectado
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T00:35:46Z
user: login: ealmloff
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-08T23:09:45Z
user: login: ealmloff
body: Looks much better! The name `axum_wasm` might be slightly confusing if you need to import it in non-wasm targets. Maybe just `axum_core` for the wasm compatible axum integration and `axum` if you have access to both axum and a filesystem for native targets targets?

I expect tests will fail here again because the default features of axum were removed from the workspace. Adding `default-features = true` to the dev dependencies that rely on them should fix the issue
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-08T23:17:00Z
user: login: ealmloff
body: Looks great! Thanks for working on this!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-10T13:29:51Z
user: login: jkelleyrtp
body: Thank you!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-27T06:51:46Z

Review comments:
diff_hunk: @@ -87,7 +87,34 @@ desktop = ["dep:dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwe
 mobile = ["dep:dioxus-mobile", "server_fn/reqwest", "dioxus_server_macro/reqwest"]
 default-tls = ["server_fn/default-tls"]
 rustls = ["server_fn/rustls", "dep:rustls", "dep:hyper-rustls"]
-axum = ["dep:axum", "dep:tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum", "default-tls"]
+axum_server_fn = [
+    "dep:axum",
+    "server_fn/ssr",
+    "dep:tokio",
+    "dep:dioxus-ssr",
+    "dep:dioxus-isrg",
+    "dep:hyper",
+    "dep:http",
+    "dep:pin-project",
+    "dep:dioxus-cli-config",
+    "dep:async-trait",
+    "server_fn/axum-no-default",
+    "dioxus_server_macro/axum",
+    "default-tls",
+    "dep:parking_lot",
+    "dioxus-interpreter-js",
+]
+axum = [
+    "dep:axum",
+    "dep:tower-http",
+    "server",
+    "server_fn/axum",
+    "axum_server_fn",
+    "dioxus_server_macro/axum",
+    "default-tls",
+    "axum/ws",
path: packages/fullstack/Cargo.toml
user: login: ealmloff
body: I don't think we use the ws or macros features any more. We could remove them entirely instead of moving them to the full axum feature
created_at: 2025-03-07T18:07:15Z
author_association: MEMBER
diff_hunk: @@ -87,7 +87,34 @@ desktop = ["dep:dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwe
 mobile = ["dep:dioxus-mobile", "server_fn/reqwest", "dioxus_server_macro/reqwest"]
 default-tls = ["server_fn/default-tls"]
 rustls = ["server_fn/rustls", "dep:rustls", "dep:hyper-rustls"]
-axum = ["dep:axum", "dep:tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum", "default-tls"]
+axum_server_fn = [
+    "dep:axum",
+    "server_fn/ssr",
+    "dep:tokio",
+    "dep:dioxus-ssr",
+    "dep:dioxus-isrg",
+    "dep:hyper",
+    "dep:http",
+    "dep:pin-project",
+    "dep:dioxus-cli-config",
+    "dep:async-trait",
+    "server_fn/axum-no-default",
+    "dioxus_server_macro/axum",
+    "default-tls",
+    "dep:parking_lot",
+    "dioxus-interpreter-js",
+]
+axum = [
+    "dep:axum",
+    "dep:tower-http",
path: packages/fullstack/Cargo.toml
user: login: ealmloff
body: Messing with dependencies, it looks like `tower-http` and `server_fn/axum` are the only dependencies that are causing compilation errors on wasm with the axum feature enabled. We only use `tower-http` in `serve_static_assets`. Instead of two duplicate traits that contain mostly duplicate methods, could we pull out that and `serve_dioxus_application` into a separate super trait behind an axum feature flag?

```rust
// WASM compatible integration
pub trait DioxusRouterServerFnExt<S> {
    fn register_server_functions(self) -> Self
    where
        Self: Sized,
    {
        self.register_server_functions_with_context(Default::default())
    }

    fn register_server_functions_with_context(self, context_providers: ContextProviders) -> Self;
}

// WASM incompatible integration
#[cfg(feature = "axum")]
pub trait DioxusRouterExt<S>: DioxusRouterServerFnExt<S> {
    fn serve_static_assets(self) -> Self
    where
        Self: Sized;

    fn serve_dioxus_application<Cfg, Error>(self, cfg: Cfg, app: fn() -> Element) -> Self
    where
        Cfg: TryInto<ServeConfig, Error = Error>,
        Error: std::error::Error,
        Self: Sized;
}
```

If we still expose [`render_handler`](https://docs.rs/dioxus-fullstack/latest/dioxus_fullstack/server/fn.render_handler.html) when compiling to wasm, you can pull in that and serve your static files to render SSR in serverless
created_at: 2025-03-07T18:16:57Z
author_association: MEMBER
diff_hunk: @@ -87,7 +87,34 @@ desktop = ["dep:dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwe
 mobile = ["dep:dioxus-mobile", "server_fn/reqwest", "dioxus_server_macro/reqwest"]
 default-tls = ["server_fn/default-tls"]
 rustls = ["server_fn/rustls", "dep:rustls", "dep:hyper-rustls"]
-axum = ["dep:axum", "dep:tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum", "default-tls"]
+axum_server_fn = [
+    "dep:axum",
+    "server_fn/ssr",
+    "dep:tokio",
+    "dep:dioxus-ssr",
+    "dep:dioxus-isrg",
+    "dep:hyper",
+    "dep:http",
+    "dep:pin-project",
+    "dep:dioxus-cli-config",
+    "dep:async-trait",
+    "server_fn/axum-no-default",
+    "dioxus_server_macro/axum",
+    "default-tls",
+    "dep:parking_lot",
+    "dioxus-interpreter-js",
+]
+axum = [
+    "dep:axum",
+    "dep:tower-http",
path: packages/fullstack/Cargo.toml
user: login: conectado
body: Yes! I like this much more :) This does break compatibility though if someone was explicitly depending on `DioxusRouterExt` right?
created_at: 2025-03-08T00:35:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,7 +87,34 @@ desktop = ["dep:dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwe
 mobile = ["dep:dioxus-mobile", "server_fn/reqwest", "dioxus_server_macro/reqwest"]
 default-tls = ["server_fn/default-tls"]
 rustls = ["server_fn/rustls", "dep:rustls", "dep:hyper-rustls"]
-axum = ["dep:axum", "dep:tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum", "default-tls"]
+axum_server_fn = [
+    "dep:axum",
+    "server_fn/ssr",
+    "dep:tokio",
+    "dep:dioxus-ssr",
+    "dep:dioxus-isrg",
+    "dep:hyper",
+    "dep:http",
+    "dep:pin-project",
+    "dep:dioxus-cli-config",
+    "dep:async-trait",
+    "server_fn/axum-no-default",
+    "dioxus_server_macro/axum",
+    "default-tls",
+    "dep:parking_lot",
+    "dioxus-interpreter-js",
+]
+axum = [
+    "dep:axum",
+    "dep:tower-http",
path: packages/fullstack/Cargo.toml
user: login: ealmloff
body: Yes, we have already started merging breaking PRs for the next release and this isn't breaking for most users so I don't think it is an issue.
created_at: 2025-03-08T23:09:45Z
author_association: MEMBER

PR comments:
user: login: conectado
created_at: 2025-03-08T01:35:55Z
body: @ealmloff Thanks for the review! updated the PR for the feedback, lmk if there's something more to adjust
author_association: CONTRIBUTOR
user: login: conectado
created_at: 2025-03-10T06:04:55Z
body: > Looks much better! The name `axum_wasm` might be slightly confusing if you need to import it in non-wasm targets. Maybe just `axum_core` for the wasm compatible axum integration and `axum` if you have access to both axum and a filesystem for native targets targets?
> 
> I expect tests will fail here again because the default features of axum were removed from the workspace. Adding `default-features = true` to the dev dependencies that rely on them should fix the issue

ah nice! I struggled to find a better name. feedback applied and also cleaned up the dependencies of each feature a bit more
author_association: CONTRIBUTOR
user: login: conectado
created_at: 2025-03-10T15:55:25Z
body: Hopefully CI should be passing now!
author_association: CONTRIBUTOR
user: login: conectado
created_at: 2025-03-10T23:06:05Z
body: my bad! now it should work 
author_association: CONTRIBUTOR
user: login: conectado
created_at: 2025-03-11T21:09:07Z
body: rebased and conflicts resolved
author_association: CONTRIBUTOR
user: login: conectado
created_at: 2025-03-12T00:02:21Z
body: I think CI failure right now is unrelated
author_association: CONTRIBUTOR
user: login: conectado
created_at: 2025-03-14T16:06:54Z
body: rebased and conflicts solved! :) 
author_association: CONTRIBUTOR
user: login: conectado
created_at: 2025-03-18T19:35:32Z
body: Just checking in on this PR—let me know if there's anything else I can do to help get it merged. No rush, just asking!
author_association: CONTRIBUTOR
user: login: ealmloff
created_at: 2025-03-18T21:43:54Z
body: > Just checking in on this PR—let me know if there's anything else I can do to help get it merged. No rush, just asking!

Looks great to me, just waiting for a review from Jon before this can be merged
author_association: MEMBER