axum_adapter.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. //! Dioxus utilities for the [Axum](https://docs.rs/axum/latest/axum/index.html) server framework.
  2. //!
  3. //! # Example
  4. //! ```rust
  5. //! #![allow(non_snake_case)]
  6. //! use dioxus_lib::prelude::*;
  7. //! use dioxus_fullstack::prelude::*;
  8. //!
  9. //! fn main() {
  10. //! #[cfg(feature = "web")]
  11. //! // Hydrate the application on the client
  12. //! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
  13. //! #[cfg(feature = "server")]
  14. //! {
  15. //! tokio::runtime::Runtime::new()
  16. //! .unwrap()
  17. //! .block_on(async move {
  18. //! let listener = tokio::net::TcpListener::bind("127.0.0.01:8080")
  19. //! .await
  20. //! .unwrap();
  21. //! axum::serve(
  22. //! listener,
  23. //! axum::Router::new()
  24. //! // Server side render the application, serve static assets, and register server functions
  25. //! .serve_dioxus_application("", ServerConfig::new(app, ()))
  26. //! .into_make_service(),
  27. //! )
  28. //! .await
  29. //! .unwrap();
  30. //! });
  31. //! }
  32. //! }
  33. //!
  34. //! fn app() -> Element {
  35. //! let mut text = use_signal(|| "...".to_string());
  36. //!
  37. //! rsx! {
  38. //! button {
  39. //! onclick: move |_| async move {
  40. //! if let Ok(data) = get_server_data().await {
  41. //! text.set(data);
  42. //! }
  43. //! },
  44. //! "Run a server function"
  45. //! }
  46. //! "Server said: {text}"
  47. //! })
  48. //! }
  49. //!
  50. //! #[server(GetServerData)]
  51. //! async fn get_server_data() -> Result<String, ServerFnError> {
  52. //! Ok("Hello from the server!".to_string())
  53. //! }
  54. //! ```
  55. use axum::routing::*;
  56. use axum::{
  57. body::{self, Body},
  58. extract::State,
  59. http::{Request, Response, StatusCode},
  60. response::IntoResponse,
  61. };
  62. use dioxus_lib::prelude::VirtualDom;
  63. use futures_util::Future;
  64. use http::header::*;
  65. use std::sync::Arc;
  66. use crate::prelude::*;
  67. /// A extension trait with utilities for integrating Dioxus with your Axum router.
  68. pub trait DioxusRouterExt<S> {
  69. /// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
  70. ///
  71. /// # Example
  72. /// ```rust
  73. /// use dioxus_lib::prelude::*;
  74. /// use dioxus_fullstack::prelude::*;
  75. ///
  76. /// #[tokio::main]
  77. /// async fn main() {
  78. /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
  79. /// axum::Server::bind(&addr)
  80. /// .serve(
  81. /// axum::Router::new()
  82. /// // Register server functions routes with the default handler
  83. /// .register_server_fns("")
  84. /// .into_make_service(),
  85. /// )
  86. /// .await
  87. /// .unwrap();
  88. /// }
  89. /// ```
  90. fn register_server_fns(self) -> Self;
  91. /// Serves the static WASM for your Dioxus application (except the generated index.html).
  92. ///
  93. /// # Example
  94. /// ```rust
  95. /// #![allow(non_snake_case)]
  96. /// use dioxus_lib::prelude::*;
  97. /// use dioxus_fullstack::prelude::*;
  98. ///
  99. /// #[tokio::main]
  100. /// async fn main() {
  101. /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
  102. /// axum::Server::bind(&addr)
  103. /// .serve(
  104. /// axum::Router::new()
  105. /// // Server side render the application, serve static assets, and register server functions
  106. /// .serve_static_assets("dist")
  107. /// // Server render the application
  108. /// // ...
  109. /// .into_make_service(),
  110. /// )
  111. /// .await
  112. /// .unwrap();
  113. /// }
  114. ///
  115. /// fn app() -> Element {
  116. /// unimplemented!()
  117. /// }
  118. /// ```
  119. fn serve_static_assets(
  120. self,
  121. assets_path: impl Into<std::path::PathBuf>,
  122. ) -> impl Future<Output = Self> + Send + Sync
  123. where
  124. Self: Sized;
  125. /// Serves the Dioxus application. This will serve a complete server side rendered application.
  126. /// This will serve static assets, server render the application, register server functions, and integrate with hot reloading.
  127. ///
  128. /// # Example
  129. /// ```rust
  130. /// #![allow(non_snake_case)]
  131. /// use dioxus_lib::prelude::*;
  132. /// use dioxus_fullstack::prelude::*;
  133. ///
  134. /// #[tokio::main]
  135. /// async fn main() {
  136. /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
  137. /// axum::Server::bind(&addr)
  138. /// .serve(
  139. /// axum::Router::new()
  140. /// // Server side render the application, serve static assets, and register server functions
  141. /// .serve_dioxus_application("", ServerConfig::new(app, ()))
  142. /// .into_make_service(),
  143. /// )
  144. /// .await
  145. /// .unwrap();
  146. /// }
  147. ///
  148. /// fn app() -> Element {
  149. /// unimplemented!()
  150. /// }
  151. /// ```
  152. fn serve_dioxus_application(
  153. self,
  154. cfg: impl Into<ServeConfig>,
  155. build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
  156. ) -> impl Future<Output = Self> + Send + Sync
  157. where
  158. Self: Sized;
  159. }
  160. impl<S> DioxusRouterExt<S> for Router<S>
  161. where
  162. S: Send + Sync + Clone + 'static,
  163. {
  164. fn register_server_fns(mut self) -> Self {
  165. use http::method::Method;
  166. for (path, method) in server_fn::axum::server_fn_paths() {
  167. tracing::trace!("Registering server function: {} {}", method, path);
  168. let handler = move |req| handle_server_fns_inner(path, || {}, req);
  169. self = match method {
  170. Method::GET => self.route(path, get(handler)),
  171. Method::POST => self.route(path, post(handler)),
  172. Method::PUT => self.route(path, put(handler)),
  173. _ => todo!(),
  174. };
  175. }
  176. self
  177. }
  178. // TODO: This is a breaking change, but we should probably serve static assets from a different directory than dist where the server executable is located
  179. // This would prevent issues like https://github.com/DioxusLabs/dioxus/issues/2327
  180. fn serve_static_assets(
  181. mut self,
  182. assets_path: impl Into<std::path::PathBuf>,
  183. ) -> impl Future<Output = Self> + Send + Sync {
  184. use tower_http::services::{ServeDir, ServeFile};
  185. let assets_path = assets_path.into();
  186. async move {
  187. // Serve all files in dist folder except index.html
  188. let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| {
  189. panic!(
  190. "Couldn't read assets directory at {:?}: {}",
  191. &assets_path, e
  192. )
  193. });
  194. for entry in dir.flatten() {
  195. let path = entry.path();
  196. if path.ends_with("index.html") {
  197. continue;
  198. }
  199. let route = path
  200. .strip_prefix(&assets_path)
  201. .unwrap()
  202. .iter()
  203. .map(|segment| {
  204. segment.to_str().unwrap_or_else(|| {
  205. panic!("Failed to convert path segment {:?} to string", segment)
  206. })
  207. })
  208. .collect::<Vec<_>>()
  209. .join("/");
  210. let route = format!("/{}", route);
  211. if path.is_dir() {
  212. self = self.nest_service(&route, ServeDir::new(path).precompressed_br());
  213. } else {
  214. self = self.nest_service(&route, ServeFile::new(path).precompressed_br());
  215. }
  216. }
  217. self
  218. }
  219. }
  220. fn serve_dioxus_application(
  221. self,
  222. cfg: impl Into<ServeConfig>,
  223. build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
  224. ) -> impl Future<Output = Self> + Send + Sync {
  225. let cfg = cfg.into();
  226. async move {
  227. let ssr_state = SSRState::new(&cfg);
  228. // Add server functions and render index.html
  229. let mut server = self
  230. .serve_static_assets(cfg.assets_path.clone())
  231. .await
  232. .register_server_fns();
  233. #[cfg(all(feature = "hot-reload", debug_assertions))]
  234. {
  235. use dioxus_hot_reload::HotReloadRouterExt;
  236. server = server.forward_cli_hot_reloading();
  237. }
  238. server.fallback(get(render_handler).with_state((
  239. cfg,
  240. Arc::new(build_virtual_dom),
  241. ssr_state,
  242. )))
  243. }
  244. }
  245. }
  246. fn apply_request_parts_to_response<B>(
  247. headers: hyper::header::HeaderMap,
  248. response: &mut axum::response::Response<B>,
  249. ) {
  250. let mut_headers = response.headers_mut();
  251. for (key, value) in headers.iter() {
  252. mut_headers.insert(key, value.clone());
  253. }
  254. }
  255. type AxumHandler<F> = (
  256. F,
  257. ServeConfig,
  258. SSRState,
  259. Arc<dyn Fn() -> VirtualDom + Send + Sync>,
  260. );
  261. /// SSR renderer handler for Axum with added context injection.
  262. ///
  263. /// # Example
  264. /// ```rust,no_run
  265. /// #![allow(non_snake_case)]
  266. /// use std::sync::{Arc, Mutex};
  267. ///
  268. /// use axum::routing::get;
  269. /// use dioxus_lib::prelude::*;
  270. /// use dioxus_fullstack::{axum_adapter::render_handler_with_context, prelude::*};
  271. ///
  272. /// fn app() -> Element {
  273. /// rsx! {
  274. /// "hello!"
  275. /// }
  276. /// }
  277. ///
  278. /// #[tokio::main]
  279. /// async fn main() {
  280. /// let cfg = ServerConfig::new(app, ())
  281. /// .assets_path("dist")
  282. /// .build();
  283. /// let ssr_state = SSRState::new(&cfg);
  284. ///
  285. /// // This could be any state you want to be accessible from your server
  286. /// // functions using `[DioxusServerContext::get]`.
  287. /// let state = Arc::new(Mutex::new("state".to_string()));
  288. ///
  289. /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
  290. /// axum::Server::bind(&addr)
  291. /// .serve(
  292. /// axum::Router::new()
  293. /// // Register server functions, etc.
  294. /// // Note you probably want to use `register_server_fns_with_handler`
  295. /// // to inject the context into server functions running outside
  296. /// // of an SSR render context.
  297. /// .fallback(get(render_handler_with_context).with_state((
  298. /// move |ctx| ctx.insert(state.clone()).unwrap(),
  299. /// cfg,
  300. /// ssr_state,
  301. /// )))
  302. /// .into_make_service(),
  303. /// )
  304. /// .await
  305. /// .unwrap();
  306. /// }
  307. /// ```
  308. pub async fn render_handler_with_context<F: FnMut(&mut DioxusServerContext)>(
  309. State((mut inject_context, cfg, ssr_state, virtual_dom_factory)): State<AxumHandler<F>>,
  310. request: Request<Body>,
  311. ) -> impl IntoResponse {
  312. let (parts, _) = request.into_parts();
  313. let url = parts.uri.path_and_query().unwrap().to_string();
  314. let parts: Arc<tokio::sync::RwLock<http::request::Parts>> =
  315. Arc::new(tokio::sync::RwLock::new(parts));
  316. let mut server_context = DioxusServerContext::new(parts.clone());
  317. inject_context(&mut server_context);
  318. match ssr_state
  319. .render(url, &cfg, move || virtual_dom_factory(), &server_context)
  320. .await
  321. {
  322. Ok(rendered) => {
  323. let crate::render::RenderResponse { html, freshness } = rendered;
  324. let mut response = axum::response::Html::from(html).into_response();
  325. freshness.write(response.headers_mut());
  326. let headers = server_context.response_parts().unwrap().headers.clone();
  327. apply_request_parts_to_response(headers, &mut response);
  328. response
  329. }
  330. Err(e) => {
  331. tracing::error!("Failed to render page: {}", e);
  332. report_err(e).into_response()
  333. }
  334. }
  335. }
  336. type RenderHandlerExtractor = (
  337. ServeConfig,
  338. Arc<dyn Fn() -> VirtualDom + Send + Sync>,
  339. SSRState,
  340. );
  341. /// SSR renderer handler for Axum
  342. pub async fn render_handler(
  343. State((cfg, virtual_dom_factory, ssr_state)): State<RenderHandlerExtractor>,
  344. request: Request<Body>,
  345. ) -> impl IntoResponse {
  346. render_handler_with_context(
  347. State((|_: &mut _| (), cfg, ssr_state, virtual_dom_factory)),
  348. request,
  349. )
  350. .await
  351. }
  352. fn report_err<E: std::fmt::Display>(e: E) -> Response<axum::body::Body> {
  353. Response::builder()
  354. .status(StatusCode::INTERNAL_SERVER_ERROR)
  355. .body(body::Body::new(format!("Error: {}", e)))
  356. .unwrap()
  357. }
  358. /// A handler for Dioxus server functions. This will run the server function and return the result.
  359. async fn handle_server_fns_inner(
  360. path: &str,
  361. additional_context: impl Fn() + 'static + Clone + Send,
  362. req: Request<Body>,
  363. ) -> impl IntoResponse {
  364. use server_fn::middleware::Service;
  365. let path_string = path.to_string();
  366. let future = move || async move {
  367. let (parts, body) = req.into_parts();
  368. let req = Request::from_parts(parts.clone(), body);
  369. if let Some(mut service) =
  370. server_fn::axum::get_server_fn_service(&path_string)
  371. {
  372. let server_context = DioxusServerContext::new(Arc::new(tokio::sync::RwLock::new(parts)));
  373. additional_context();
  374. // store Accepts and Referrer in case we need them for redirect (below)
  375. let accepts_html = req
  376. .headers()
  377. .get(ACCEPT)
  378. .and_then(|v| v.to_str().ok())
  379. .map(|v| v.contains("text/html"))
  380. .unwrap_or(false);
  381. let referrer = req.headers().get(REFERER).cloned();
  382. // actually run the server fn (which may use the server context)
  383. let mut res = ProvideServerContext::new(service.run(req), server_context.clone()).await;
  384. // it it accepts text/html (i.e., is a plain form post) and doesn't already have a
  385. // Location set, then redirect to Referer
  386. if accepts_html {
  387. if let Some(referrer) = referrer {
  388. let has_location = res.headers().get(LOCATION).is_some();
  389. if !has_location {
  390. *res.status_mut() = StatusCode::FOUND;
  391. res.headers_mut().insert(LOCATION, referrer);
  392. }
  393. }
  394. }
  395. // apply the response parts from the server context to the response
  396. let mut res_options = server_context.response_parts_mut().unwrap();
  397. res.headers_mut().extend(res_options.headers.drain());
  398. Ok(res)
  399. } else {
  400. Response::builder().status(StatusCode::BAD_REQUEST).body(
  401. {
  402. #[cfg(target_family = "wasm")]
  403. {
  404. Body::from(format!(
  405. "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.",
  406. ))
  407. }
  408. #[cfg(not(target_family = "wasm"))]
  409. {
  410. Body::from(format!(
  411. "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.",
  412. ))
  413. }
  414. }
  415. )
  416. }
  417. .expect("could not build Response")
  418. };
  419. #[cfg(target_arch = "wasm32")]
  420. {
  421. use futures_util::future::FutureExt;
  422. let result = tokio::task::spawn_local(future);
  423. let result = result.then(|f| async move { f.unwrap() });
  424. result.await.unwrap_or_else(|e| {
  425. use server_fn::error::NoCustomError;
  426. use server_fn::error::ServerFnErrorSerde;
  427. (
  428. StatusCode::INTERNAL_SERVER_ERROR,
  429. ServerFnError::<NoCustomError>::ServerError(e.to_string())
  430. .ser()
  431. .unwrap_or_default(),
  432. )
  433. .into_response()
  434. })
  435. }
  436. #[cfg(not(target_arch = "wasm32"))]
  437. {
  438. future().await
  439. }
  440. }