Skip to main content

dioxus_server/
server.rs

1use crate::{
2    ServeConfig, ServerFunction,
3    ssr::{SSRError, SsrRendererPool},
4};
5use axum::{
6    body::Body,
7    extract::State,
8    http::{Request, StatusCode},
9    response::{IntoResponse, Response},
10    routing::*,
11};
12use dioxus_core::{ComponentFunction, VirtualDom};
13use http::header::*;
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16use tokio_util::task::LocalPoolHandle;
17use tower::ServiceExt;
18use tower::util::MapResponse;
19use tower_http::services::fs::ServeFileSystemResponseBody;
20
21/// A extension trait with utilities for integrating Dioxus with your Axum router.
22pub trait DioxusRouterExt {
23    /// Serves the static WASM for your Dioxus application (except the generated index.html).
24    ///
25    /// # Example
26    /// ```rust, no_run
27    /// # #![allow(non_snake_case)]
28    /// # use dioxus::prelude::*;
29    /// use dioxus_server::DioxusRouterExt;
30    ///
31    /// #[tokio::main]
32    /// async fn main() -> anyhow::Result<()> {
33    ///     let addr = dioxus::cli_config::fullstack_address_or_localhost();
34    ///     let router = axum::Router::new()
35    ///         // Server side render the application, serve static assets, and register server functions
36    ///         .serve_static_assets()
37    ///         // Server render the application
38    ///         // ...
39    ///         .with_state(dioxus_server::FullstackState::headless());
40    ///     let listener = tokio::net::TcpListener::bind(addr).await?;
41    ///     axum::serve(listener, router).await?;
42    ///     Ok(())
43    /// }
44    /// ```
45    fn serve_static_assets(self) -> Router<FullstackState>;
46
47    /// Serves the Dioxus application. This will serve a complete server side rendered application.
48    /// This will serve static assets, server render the application, register server functions, and integrate with hot reloading.
49    ///
50    /// # Example
51    /// ```rust, no_run
52    /// # #![allow(non_snake_case)]
53    /// # use dioxus::prelude::*;
54    /// use dioxus_server::{DioxusRouterExt, ServeConfig};
55    ///
56    /// #[tokio::main]
57    /// async fn main() {
58    ///     let addr = dioxus::cli_config::fullstack_address_or_localhost();
59    ///     let router = axum::Router::new()
60    ///         // Server side render the application, serve static assets, and register server functions
61    ///         .serve_dioxus_application(ServeConfig::new(), app);
62    ///     let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
63    ///     axum::serve(listener, router).await.unwrap();
64    /// }
65    ///
66    /// fn app() -> Element {
67    ///     rsx! { "Hello World" }
68    /// }
69    /// ```
70    fn serve_dioxus_application<M: 'static>(
71        self,
72        cfg: ServeConfig,
73        app: impl ComponentFunction<(), M> + Send + Sync,
74    ) -> Router<()>;
75
76    /// Registers server functions with the default handler.
77    ///
78    /// # Example
79    /// ```rust, no_run
80    /// # use dioxus::prelude::*;
81    /// # use dioxus_server::DioxusRouterExt;
82    /// #[tokio::main]
83    /// async fn main() {
84    ///     let addr = dioxus::cli_config::fullstack_address_or_localhost();
85    ///     let router = axum::Router::new()
86    ///         // Register server functions routes with the default handler
87    ///         .register_server_functions()
88    ///         .with_state(dioxus_server::FullstackState::headless());
89    ///     let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
90    ///     axum::serve(listener, router).await.unwrap();
91    /// }
92    /// ```
93    #[allow(dead_code)]
94    fn register_server_functions(self) -> Router<FullstackState>;
95
96    /// Serves a Dioxus application without static assets.
97    /// Sets up server function routes and rendering endpoints only.
98    ///
99    /// Useful for WebAssembly environments or when static assets
100    /// are served by another system.
101    ///
102    /// # Example
103    /// ```rust, no_run
104    /// # use dioxus::prelude::*;
105    /// # use dioxus_server::{DioxusRouterExt, ServeConfig};
106    /// #[tokio::main]
107    /// async fn main() {
108    ///     let router = axum::Router::new()
109    ///         .serve_api_application(ServeConfig::new(), app)
110    ///         .into_make_service();
111    ///     // ...
112    /// }
113    ///
114    /// fn app() -> Element {
115    ///     rsx! { "Hello World" }
116    /// }
117    /// ```
118    fn serve_api_application<M: 'static>(
119        self,
120        cfg: ServeConfig,
121        app: impl ComponentFunction<(), M> + Send + Sync,
122    ) -> Router<()>
123    where
124        Self: Sized;
125}
126
127#[cfg(not(target_arch = "wasm32"))]
128impl DioxusRouterExt for Router<FullstackState> {
129    fn register_server_functions(mut self) -> Router<FullstackState> {
130        use std::collections::HashSet;
131
132        let mut seen = HashSet::new();
133
134        for func in ServerFunction::collect() {
135            if seen.insert(format!("{} {}", func.method(), func.path())) {
136                tracing::info!("Registering: {} {}", func.method(), func.path());
137
138                self = self.route(func.path(), func.method_router())
139            }
140        }
141
142        self
143    }
144
145    fn serve_static_assets(self) -> Router<FullstackState> {
146        let Some(public_path) = public_path() else {
147            return self;
148        };
149
150        // Serve all files in public folder except index.html
151        serve_dir_cached(self, &public_path, &public_path)
152    }
153
154    fn serve_api_application<M: 'static>(
155        self,
156        cfg: ServeConfig,
157        app: impl ComponentFunction<(), M> + Send + Sync,
158    ) -> Router<()> {
159        self.register_server_functions()
160            .fallback(get(FullstackState::render_handler))
161            .with_state(FullstackState::new(cfg, app))
162    }
163
164    fn serve_dioxus_application<M: 'static>(
165        self,
166        cfg: ServeConfig,
167        app: impl ComponentFunction<(), M> + Send + Sync,
168    ) -> Router<()> {
169        self.register_server_functions()
170            .serve_static_assets()
171            .fallback(get(FullstackState::render_handler))
172            .with_state(FullstackState::new(cfg, app))
173    }
174}
175
176/// SSR renderer handler for Axum with added context injection.
177///
178/// # Example
179/// ```rust,no_run
180/// #![allow(non_snake_case)]
181/// use std::sync::{Arc, Mutex};
182///
183/// use axum::routing::get;
184/// use dioxus::prelude::*;
185/// use dioxus_server::{FullstackState, render_handler, ServeConfig};
186///
187/// fn app() -> Element {
188///     rsx! {
189///         "hello!"
190///     }
191/// }
192///
193/// #[tokio::main]
194/// async fn main() {
195///     let addr = dioxus::cli_config::fullstack_address_or_localhost();
196///     let router = axum::Router::new()
197///         // Register server functions, etc.
198///         // Note you can use `register_server_functions_with_context`
199///         // to inject the context into server functions running outside
200///         // of an SSR render context.
201///         .fallback(get(render_handler))
202///         .with_state(FullstackState::new(ServeConfig::new(), app));
203///
204///     let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
205///     axum::serve(listener, router).await.unwrap();
206/// }
207/// ```
208pub async fn render_handler(
209    State(state): State<FullstackState>,
210    request: Request<Body>,
211) -> impl IntoResponse {
212    FullstackState::render_handler(State(state), request).await
213}
214
215/// State used by [`FullstackState::render_handler`] to render a dioxus component with axum
216#[derive(Clone)]
217pub struct FullstackState {
218    config: ServeConfig,
219    build_virtual_dom: Arc<dyn Fn() -> VirtualDom + Send + Sync>,
220    renderers: Arc<SsrRendererPool>,
221    pub(crate) rt: LocalPoolHandle,
222}
223
224impl FullstackState {
225    /// Create a headless [`FullstackState`] without a root component.
226    ///
227    /// This won't render pages, but can still be used to register server functions and serve static assets.
228    pub fn headless() -> Self {
229        let rt = LocalPoolHandle::new(
230            std::thread::available_parallelism()
231                .map(usize::from)
232                .unwrap_or(1),
233        );
234
235        Self {
236            renderers: Arc::new(SsrRendererPool::new(4, None)),
237            build_virtual_dom: Arc::new(|| {
238                panic!("No root component provided for headless FullstackState")
239            }),
240            config: ServeConfig::new(),
241            rt,
242        }
243    }
244
245    /// Create a new [`FullstackState`]
246    pub fn new<M: 'static>(
247        config: ServeConfig,
248        root: impl ComponentFunction<(), M> + Send + Sync + 'static,
249    ) -> Self {
250        let rt = LocalPoolHandle::new(
251            std::thread::available_parallelism()
252                .map(usize::from)
253                .unwrap_or(1),
254        );
255
256        Self {
257            renderers: Arc::new(SsrRendererPool::new(4, config.incremental.clone())),
258            build_virtual_dom: Arc::new(move || VirtualDom::new_with_props(root.clone(), ())),
259            config,
260            rt,
261        }
262    }
263
264    /// Create a new [`FullstackState`] with a custom [`VirtualDom`] factory. This method can be
265    /// used to pass context into the root component of your application.
266    pub fn new_with_virtual_dom_factory(
267        config: ServeConfig,
268        build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
269    ) -> Self {
270        let rt = LocalPoolHandle::new(
271            std::thread::available_parallelism()
272                .map(usize::from)
273                .unwrap_or(1),
274        );
275
276        Self {
277            renderers: Arc::new(SsrRendererPool::new(4, config.incremental.clone())),
278            config,
279            build_virtual_dom: Arc::new(build_virtual_dom),
280            rt,
281        }
282    }
283
284    /// Set the [`ServeConfig`] for this [`FullstackState`]
285    pub fn with_config(mut self, config: ServeConfig) -> Self {
286        self.config = config;
287        self
288    }
289
290    /// SSR renderer handler for Axum with added context injection.
291    ///
292    /// # Example
293    /// ```rust,no_run
294    /// #![allow(non_snake_case)]
295    /// use std::sync::{Arc, Mutex};
296    ///
297    /// use axum::routing::get;
298    /// use dioxus::prelude::*;
299    /// use dioxus_server::{FullstackState, render_handler, ServeConfig};
300    ///
301    /// fn app() -> Element {
302    ///     rsx! {
303    ///         "hello!"
304    ///     }
305    /// }
306    ///
307    /// #[tokio::main]
308    /// async fn main() {
309    ///     let addr = dioxus::cli_config::fullstack_address_or_localhost();
310    ///     let router = axum::Router::new()
311    ///         // Register server functions, etc.
312    ///         // Note you can use `register_server_functions_with_context`
313    ///         // to inject the context into server functions running outside
314    ///         // of an SSR render context.
315    ///         .fallback(get(render_handler))
316    ///         .with_state(FullstackState::new(ServeConfig::new(), app))
317    ///         .into_make_service();
318    ///
319    ///     let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
320    ///     axum::serve(listener, router).await.unwrap();
321    /// }
322    /// ```
323    pub async fn render_handler(State(state): State<Self>, request: Request<Body>) -> Response {
324        let (parts, _) = request.into_parts();
325
326        let response = state
327            .renderers
328            .clone()
329            .render_to(parts, &state.config, &state.rt, {
330                let build_virtual_dom = state.build_virtual_dom.clone();
331                let context_providers = state.config.context_providers.clone();
332                move || {
333                    let mut vdom = build_virtual_dom();
334                    for state in context_providers.as_slice() {
335                        vdom.insert_any_root_context(state());
336                    }
337                    vdom
338                }
339            })
340            .await;
341
342        match response {
343            Ok((status, headers, freshness, rx)) => {
344                let mut response = Response::builder()
345                    .status(status.status)
346                    .header(CONTENT_TYPE, "text/html; charset=utf-8")
347                    .body(Body::from_stream(rx))
348                    .unwrap();
349
350                // Write our freshness header
351                freshness.write(response.headers_mut());
352
353                // write the other headers set by the user
354                for (key, value) in headers.into_iter() {
355                    if let Some(key) = key {
356                        response.headers_mut().insert(key, value);
357                    }
358                }
359
360                response
361            }
362
363            Err(SSRError::Incremental(e)) => {
364                tracing::error!("Failed to render page: {}", e);
365
366                Response::builder()
367                    .status(StatusCode::INTERNAL_SERVER_ERROR)
368                    .body(e.to_string())
369                    .unwrap()
370                    .into_response()
371            }
372
373            Err(SSRError::HttpError { status, message }) => Response::builder()
374                .status(status)
375                .body(Body::from(message.unwrap_or_else(|| {
376                    status
377                        .canonical_reason()
378                        .unwrap_or("An unknown error occurred")
379                        .to_string()
380                })))
381                .unwrap(),
382        }
383    }
384}
385
386/// Get the path to the public assets directory to serve static files from
387pub(crate) fn public_path() -> Option<PathBuf> {
388    if let Ok(path) = std::env::var("DIOXUS_PUBLIC_PATH") {
389        return Some(PathBuf::from(path));
390    }
391
392    // The CLI always bundles static assets into the exe/public directory
393    Some(
394        std::env::current_exe()
395            .ok()?
396            .parent()
397            .unwrap()
398            .join("public"),
399    )
400}
401
402fn serve_dir_cached<S>(mut router: Router<S>, public_path: &Path, directory: &Path) -> Router<S>
403where
404    S: Send + Sync + Clone + 'static,
405{
406    use tower_http::services::{ServeDir, ServeFile};
407
408    let dir = std::fs::read_dir(directory)
409        .unwrap_or_else(|e| panic!("Couldn't read public directory at {:?}: {}", &directory, e));
410
411    for entry in dir.flatten() {
412        let path = entry.path();
413        // Don't serve the index.html file. The SSR handler will generate it.
414        if path == public_path.join("index.html") {
415            continue;
416        }
417
418        let route = format!(
419            "/{}",
420            path.strip_prefix(public_path)
421                .unwrap()
422                .iter()
423                .map(|segment| segment.to_string_lossy())
424                .collect::<Vec<_>>()
425                .join("/")
426        );
427
428        if path.is_dir() {
429            // In debug builds, serve directories dynamically so new files (like
430            // wasm patch modules) are immediately visible without rebuilding
431            // the router. In release, keep the previous cached traversal
432            // behaviour.
433            #[cfg(debug_assertions)]
434            {
435                router = router.nest_service(&route, ServeDir::new(&path));
436            }
437
438            #[cfg(not(debug_assertions))]
439            {
440                router = serve_dir_cached(router, public_path, &path);
441            }
442        } else {
443            let serve_file = ServeFile::new(&path).precompressed_br();
444            // All cached assets are served at the root of the asset directory. If we know an asset
445            // is hashed for cache busting, we can cache the response on the client side forever. If
446            // the asset changes, the hash in the path will also change and the client will refetch it.
447            if file_name_looks_immutable(&route) {
448                router = router.nest_service(&route, cache_response_forever(serve_file))
449            } else {
450                router = router.nest_service(&route, serve_file)
451            }
452        }
453    }
454
455    router
456}
457
458type MappedAxumService<S> = MapResponse<
459    S,
460    fn(Response<ServeFileSystemResponseBody>) -> Response<ServeFileSystemResponseBody>,
461>;
462
463fn cache_response_forever<S>(service: S) -> MappedAxumService<S>
464where
465    S: ServiceExt<Request<Body>, Response = Response<ServeFileSystemResponseBody>>,
466{
467    service.map_response(|mut response: Response<ServeFileSystemResponseBody>| {
468        response.headers_mut().insert(
469            CACHE_CONTROL,
470            HeaderValue::from_static("public, max-age=31536000, immutable"),
471        );
472        response
473    })
474}
475
476fn file_name_looks_immutable(file_name: &str) -> bool {
477    // Check if the file name looks like a hash (e.g., "main-dxh12345678.js")
478    file_name.rsplit_once("-dxh").is_some_and(|(_, hash)| {
479        hash.chars()
480            .take_while(|c| *c != '.')
481            .all(|c| c.is_ascii_hexdigit())
482    })
483}
484
485#[test]
486fn test_file_name_looks_immutable() {
487    assert!(file_name_looks_immutable("main-dxh12345678.js"));
488    assert!(file_name_looks_immutable("style-dxhabcdef.css"));
489    assert!(!file_name_looks_immutable("index.html"));
490    assert!(!file_name_looks_immutable("script.js"));
491    assert!(!file_name_looks_immutable("main-dxh1234wyz.js"));
492    assert!(!file_name_looks_immutable("main-dxh12345678-invalid.js"));
493}