Skip to main content

dioxus_server/
launch.rs

1//! A launch function that creates an axum router for the LaunchBuilder
2
3use crate::{FullstackState, ServeConfig, server::DioxusRouterExt};
4use anyhow::Context;
5use axum::{
6    Router,
7    body::Body,
8    extract::{Request, State},
9    routing::IntoMakeService,
10};
11use dioxus_cli_config::base_path;
12use dioxus_core::{ComponentFunction, Element};
13
14use dioxus_devtools::{DevserverMsg, HotReloadMsg};
15use futures_util::{StreamExt, stream::FusedStream};
16use hyper::body::Incoming;
17use hyper_util::server::conn::auto::Builder as HyperBuilder;
18use hyper_util::{
19    rt::{TokioExecutor, TokioIo},
20    service::TowerToHyperService,
21};
22use std::{any::Any, net::SocketAddr, prelude::rust_2024::Future};
23use std::{pin::Pin, sync::Arc};
24use subsecond::HotFn;
25use tokio_util::either::Either;
26use tower::{Service, ServiceExt as _};
27
28#[cfg(not(target_arch = "wasm32"))]
29use {
30    dioxus_core::{RenderError, VNode},
31    tokio::net::TcpListener,
32};
33
34type ContextList = Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>;
35type BaseComp = fn() -> Element;
36
37/// Launch a fullstack app with the given root component.
38pub fn launch(root: BaseComp) -> ! {
39    launch_cfg(root, vec![], vec![])
40}
41
42/// Launch a fullstack app with the given root component, contexts, and config.
43#[allow(unused)]
44pub fn launch_cfg(root: BaseComp, contexts: ContextList, platform_config: Vec<Box<dyn Any>>) -> ! {
45    #[cfg(not(target_arch = "wasm32"))]
46    tokio::runtime::Runtime::new()
47        .unwrap()
48        .block_on(async move { serve_server(root, contexts, platform_config).await });
49
50    unreachable!("Launching a fullstack app should never return")
51}
52
53#[cfg(not(target_arch = "wasm32"))]
54async fn serve_server(
55    original_root: fn() -> Result<VNode, RenderError>,
56    contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
57    platform_config: Vec<Box<dyn Any>>,
58) {
59    let mut cfg = platform_config
60        .into_iter()
61        .find_map(|cfg| cfg.downcast::<ServeConfig>().ok().map(|b| *b))
62        .unwrap_or_else(ServeConfig::new);
63
64    // Extend the config's context providers with the context providers from the launch builder
65    for ctx in contexts {
66        let arced = Arc::new(ctx) as Arc<dyn Fn() -> Box<dyn Any> + Send + Sync>;
67        cfg.context_providers.push(arced);
68    }
69
70    let cb = move || {
71        let cfg = cfg.clone();
72        Box::pin(async move {
73            Ok(apply_base_path(
74                Router::new().serve_dioxus_application(cfg.clone(), original_root),
75                original_root,
76                cfg.clone(),
77                base_path().map(|s| s.to_string()),
78            ))
79        }) as _
80    };
81
82    serve_router(cb, dioxus_cli_config::fullstack_address_or_localhost()).await;
83}
84
85/// Create a router that serves the dioxus application at the appropriate base path.
86///
87/// This method automatically setups up:
88/// - Static asset serving
89/// - Mapping of base paths
90/// - Automatic registration of server functions
91/// - Handler to render the dioxus application
92/// - WebSocket handling for live reload and devtools
93/// - Hot-reloading
94/// - Async Runtime
95/// - Logging
96pub fn router(app: fn() -> Element) -> Router {
97    let cfg = ServeConfig::new();
98    apply_base_path(
99        Router::new().serve_dioxus_application(cfg.clone(), app),
100        app,
101        cfg,
102        base_path().map(|s| s.to_string()),
103    )
104}
105
106/// Serve a fullstack dioxus application with a custom axum router.
107///
108/// This function sets up an async runtime, enables the default dioxus logger, runs the provided initializer,
109/// and then starts an axum server with the returned router.
110///
111/// The axum router will be bound to the address specified by the `IP` and `PORT` environment variables,
112/// defaulting to `127.0.0.1:8080` if not set.
113///
114/// This function uses axum to block on serving the application, and will not return.
115pub fn serve<F>(mut serve_it: impl FnMut() -> F) -> !
116where
117    F: Future<Output = Result<Router, anyhow::Error>> + 'static,
118{
119    let cb = move || Box::pin(serve_it()) as _;
120
121    block_on(
122        async move { serve_router(cb, dioxus_cli_config::fullstack_address_or_localhost()).await },
123    );
124
125    unreachable!("Serving a fullstack app should never return")
126}
127
128/// Serve a fullstack dioxus application with a custom axum router.
129///
130/// This function enables the dioxus logger and then serves the axum server with hot-reloading support.
131///
132/// To enable hot-reloading of the router, the provided `serve_callback` should return a new `Router`
133/// each time it is called.
134pub async fn serve_router(
135    mut serve_callback: impl FnMut() -> Pin<Box<dyn Future<Output = Result<Router, anyhow::Error>>>>,
136    addr: SocketAddr,
137) {
138    dioxus_logger::initialize_default();
139
140    let listener = TcpListener::bind(addr)
141        .await
142        .with_context(|| format!("Failed to bind to address {addr}"))
143        .unwrap();
144
145    // If we're not in debug mode, just serve the app normally
146    if !cfg!(debug_assertions) {
147        axum::serve(listener, serve_callback().await.unwrap())
148            .await
149            .unwrap();
150        return;
151    }
152
153    // Wire up the devtools connection. The sender only sends messages in dev.
154    let (devtools_tx, mut devtools_rx) = futures_channel::mpsc::unbounded();
155    dioxus_devtools::connect(move |msg| _ = devtools_tx.unbounded_send(msg));
156
157    let mut hot_serve_callback = HotFn::current(serve_callback);
158    let mut make_service = hot_serve_callback
159        .call(())
160        .await
161        .map(|router| router.into_make_service())
162        .unwrap();
163
164    let (shutdown_tx, _) = tokio::sync::broadcast::channel(1);
165    let our_build_id = Some(dioxus_cli_config::build_id());
166
167    // Manually loop on accepting connections so we can also respond to devtools messages
168    loop {
169        let res = tokio::select! {
170            res = listener.accept() => Either::Left(res),
171            Some(msg) = devtools_rx.next(), if !devtools_rx.is_terminated() => Either::Right(msg),
172            else => continue
173        };
174
175        match res {
176            Either::Left(Ok((tcp_stream, _remote_addr))) => {
177                let mut make_service = make_service.clone();
178                let mut shutdown_rx = shutdown_tx.subscribe();
179
180                tokio::task::spawn(async move {
181                    let tcp_stream = TokioIo::new(tcp_stream);
182
183                    std::future::poll_fn(|cx| {
184                        <IntoMakeService<Router> as tower::Service<Request>>::poll_ready(
185                            &mut make_service,
186                            cx,
187                        )
188                    })
189                    .await
190                    .expect("Infallible");
191
192                    // upgrades needed for websockets
193                    let builder = HyperBuilder::new(TokioExecutor::new());
194                    let connection = builder.serve_connection_with_upgrades(
195                        tcp_stream,
196                        TowerToHyperService::new(
197                            make_service
198                                .call(())
199                                .await
200                                .unwrap()
201                                .map_request(|req: Request<Incoming>| req.map(Body::new)),
202                        ),
203                    );
204
205                    tokio::select! {
206                        res = connection => {
207                            if let Err(_err) = res {
208                                // This error only appears when the client doesn't send a request and
209                                // terminate the connection.
210                                //
211                                // If client sends one request then terminate connection whenever, it doesn't
212                                // appear.
213                            }
214                        }
215                        _res = shutdown_rx.recv() => {}
216                    }
217                });
218            }
219
220            // Handle just hot-patches for now.
221            // We don't do RSX hot-reload since usually the client handles that once the page is loaded.
222            //
223            // todo(jon): I *believe* SSR is resilient to RSX changes, but we should verify that...
224            Either::Right(DevserverMsg::HotReload(HotReloadMsg {
225                jump_table: Some(table),
226                for_build_id,
227                ..
228            })) if for_build_id == our_build_id => {
229                // Apply the hot-reload patch to the dioxus devtools first
230                unsafe { dioxus_devtools::subsecond::apply_patch(table).unwrap() };
231
232                // Now recreate the router
233                // We panic here because we don't want their app to continue in a maybe-corrupted state
234                make_service = hot_serve_callback
235                    .call(())
236                    .await
237                    .expect("Failed to create new router after hot-patch!")
238                    .into_make_service();
239
240                // Make sure to wipe out the renderer state so we don't have stale elements
241                crate::document::reset_renderer();
242
243                _ = shutdown_tx.send(());
244            }
245
246            // Explicitly don't handle RSX hot-reloads on the server
247            // The client will handle that once the page is loaded. If we handled it here,
248            _ => {}
249        }
250    }
251}
252
253fn block_on<T>(app_future: impl Future<Output = T>) {
254    if let Ok(handle) = tokio::runtime::Handle::try_current() {
255        handle.block_on(app_future);
256    } else {
257        tokio::runtime::Builder::new_multi_thread()
258            .enable_all()
259            .build()
260            .unwrap()
261            .block_on(app_future);
262    }
263}
264
265fn apply_base_path<M: 'static>(
266    mut router: Router,
267    root: impl ComponentFunction<(), M> + Send + Sync,
268    cfg: ServeConfig,
269    base_path: Option<String>,
270) -> Router {
271    if let Some(base_path) = base_path {
272        let base_path = base_path.trim_matches('/');
273
274        // If there is a base path, nest the router under it and serve the root route manually
275        // Nesting a route in axum only serves /base_path or /base_path/ not both
276        router = Router::new().nest(&format!("/{base_path}/"), router).route(
277            &format!("/{base_path}"),
278            axum::routing::method_routing::get(
279                |state: State<FullstackState>, mut request: Request<Body>| async move {
280                    // The root of the base path always looks like the root from dioxus fullstack
281                    *request.uri_mut() = "/".parse().unwrap();
282                    FullstackState::render_handler(state, request).await
283                },
284            )
285            .with_state(FullstackState::new(cfg, root)),
286        )
287    }
288
289    router
290}