warp_adapter.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. //! Dioxus utilities for the [Warp](https://docs.rs/warp/latest/warp/index.html) server framework.
  2. //!
  3. //! # Example
  4. //! ```rust
  5. //! #![allow(non_snake_case)]
  6. //! use dioxus::prelude::*;
  7. //! use dioxus_fullstack::prelude::*;
  8. //!
  9. //! fn main() {
  10. //! #[cfg(feature = "web")]
  11. //! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
  12. //! #[cfg(feature = "ssr")]
  13. //! {
  14. //! tokio::runtime::Runtime::new()
  15. //! .unwrap()
  16. //! .block_on(async move {
  17. //! let routes = serve_dioxus_application("", ServeConfigBuilder::new(app, ()));
  18. //! warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
  19. //! });
  20. //! }
  21. //! }
  22. //!
  23. //! fn app(cx: Scope) -> Element {
  24. //! let text = use_state(cx, || "...".to_string());
  25. //!
  26. //! cx.render(rsx! {
  27. //! button {
  28. //! onclick: move |_| {
  29. //! to_owned![text];
  30. //! async move {
  31. //! if let Ok(data) = get_server_data().await {
  32. //! text.set(data);
  33. //! }
  34. //! }
  35. //! },
  36. //! "Run a server function"
  37. //! }
  38. //! "Server said: {text}"
  39. //! })
  40. //! }
  41. //!
  42. //! #[server(GetServerData)]
  43. //! async fn get_server_data() -> Result<String, ServerFnError> {
  44. //! Ok("Hello from the server!".to_string())
  45. //! }
  46. //!
  47. //! ```
  48. use crate::layer::Service;
  49. use crate::{
  50. prelude::*, render::SSRState, serve_config::ServeConfig, server_fn::DioxusServerFnRegistry,
  51. };
  52. use crate::server_fn_service;
  53. use server_fn::{Encoding, Payload, ServerFunctionRegistry};
  54. use std::error::Error;
  55. use std::sync::Arc;
  56. use std::sync::RwLock;
  57. use tokio::task::spawn_blocking;
  58. use warp::path::FullPath;
  59. use warp::Rejection;
  60. use warp::{
  61. filters::BoxedFilter,
  62. http::{Response, StatusCode},
  63. hyper::body::Bytes,
  64. path, Filter, Reply,
  65. };
  66. /// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request.
  67. ///
  68. /// # Example
  69. /// ```rust
  70. /// use warp::{body, header, hyper::HeaderMap, path, post, Filter};
  71. ///
  72. /// #[tokio::main]
  73. /// async fn main() {
  74. /// let routes = register_server_fns_with_handler(server_fn_route, |full_route, func| {
  75. /// path(full_route)
  76. /// .and(warp::post().or(warp::get()).unify())
  77. /// .and(request_parts())
  78. /// .and(warp::body::bytes())
  79. /// .and_then(move |parts, bytes: bytes::Bytes| {
  80. /// let mut service = server_fn_service(DioxusServerContext::default(), func.clone());
  81. /// async move {
  82. /// let req = warp::hyper::Request::from_parts(parts, bytes.into());
  83. /// service.run(req).await.map_err(|err| {
  84. /// tracing::error!("Server function error: {}", err);
  85. /// warp::reject::reject()
  86. /// })
  87. /// }
  88. /// })
  89. /// })
  90. /// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
  91. /// }
  92. /// ```
  93. pub fn register_server_fns_with_handler<H, F, R>(
  94. server_fn_route: &'static str,
  95. mut handler: H,
  96. ) -> BoxedFilter<(R,)>
  97. where
  98. H: FnMut(String, server_fn::ServerFnTraitObj<()>) -> F,
  99. F: Filter<Extract = (R,), Error = warp::Rejection> + Send + Sync + 'static,
  100. F::Extract: Send,
  101. R: Reply + 'static,
  102. {
  103. let mut filter: Option<BoxedFilter<F::Extract>> = None;
  104. for server_fn_path in DioxusServerFnRegistry::paths_registered() {
  105. let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
  106. let full_route = format!("{server_fn_route}/{server_fn_path}")
  107. .trim_start_matches('/')
  108. .to_string();
  109. let route = handler(full_route, func).boxed();
  110. if let Some(boxed_filter) = filter.take() {
  111. filter = Some(boxed_filter.or(route).unify().boxed());
  112. } else {
  113. filter = Some(route);
  114. }
  115. }
  116. filter.expect("No server functions found")
  117. }
  118. /// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
  119. ///
  120. /// # Example
  121. /// ```rust
  122. /// use dioxus_fullstack::prelude::*;
  123. ///
  124. /// #[tokio::main]
  125. /// async fn main() {
  126. /// let routes = register_server_fns("");
  127. /// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
  128. /// }
  129. /// ```
  130. pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> {
  131. register_server_fns_with_handler(server_fn_route, |full_route, func| {
  132. path(full_route)
  133. .and(warp::post().or(warp::get()).unify())
  134. .and(request_parts())
  135. .and(warp::body::bytes())
  136. .and_then(move |parts, bytes: bytes::Bytes| {
  137. let mut service = server_fn_service(DioxusServerContext::default(), func.clone());
  138. async move {
  139. let req = warp::hyper::Request::from_parts(parts, bytes.into());
  140. service.run(req).await.map_err(|err| {
  141. tracing::error!("Server function error: {}", err);
  142. struct WarpServerFnError(String);
  143. impl std::fmt::Debug for WarpServerFnError {
  144. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  145. write!(f, "{}", self.0)
  146. }
  147. }
  148. impl warp::reject::Reject for WarpServerFnError {}
  149. warp::reject::custom(WarpServerFnError(err.to_string()))
  150. })
  151. }
  152. })
  153. })
  154. }
  155. /// Serves the Dioxus application. This will serve a complete server side rendered application.
  156. /// This will serve static assets, server render the application, register server functions, and intigrate with hot reloading.
  157. ///
  158. /// # Example
  159. /// ```rust
  160. /// #![allow(non_snake_case)]
  161. /// use dioxus::prelude::*;
  162. /// use dioxus_fullstack::prelude::*;
  163. ///
  164. /// #[tokio::main]
  165. /// async fn main() {
  166. /// let routes = serve_dioxus_application("", ServeConfigBuilder::new(app, ()));
  167. /// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
  168. /// }
  169. ///
  170. /// fn app(cx: Scope) -> Element {
  171. /// todo!()
  172. /// }
  173. /// ```
  174. pub fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
  175. server_fn_route: &'static str,
  176. cfg: impl Into<ServeConfig<P>>,
  177. ) -> BoxedFilter<(impl Reply,)> {
  178. let cfg = cfg.into();
  179. // Serve the dist folder and the index.html file
  180. let serve_dir = warp::fs::dir(cfg.assets_path);
  181. // Copy over any assets we find
  182. crate::collect_assets::copy_assets();
  183. connect_hot_reload()
  184. // First register the server functions
  185. .or(register_server_fns(server_fn_route))
  186. // Then the index route
  187. .or(path::end().and(render_ssr(cfg.clone())))
  188. // Then the static assets
  189. .or(serve_dir)
  190. // Then all other routes
  191. .or(render_ssr(cfg))
  192. .boxed()
  193. }
  194. /// Server render the application.
  195. pub fn render_ssr<P: Clone + serde::Serialize + Send + Sync + 'static>(
  196. cfg: ServeConfig<P>,
  197. ) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
  198. warp::get()
  199. .and(request_parts())
  200. .and(with_ssr_state(&cfg))
  201. .then(move |parts: http::request::Parts, renderer: SSRState| {
  202. let route = parts.uri.path().to_string();
  203. let parts = Arc::new(RwLock::new(parts));
  204. let cfg = cfg.clone();
  205. async move {
  206. let server_context = DioxusServerContext::new(parts);
  207. match renderer.render(route, &cfg, &server_context).await {
  208. Ok(rendered) => {
  209. let crate::render::RenderResponse { html, freshness } = rendered;
  210. let mut res = Response::builder()
  211. .header("Content-Type", "text/html")
  212. .body(html)
  213. .unwrap();
  214. let headers_mut = res.headers_mut();
  215. let headers = server_context.response_parts().unwrap().headers.clone();
  216. for (key, value) in headers.iter() {
  217. headers_mut.insert(key, value.clone());
  218. }
  219. freshness.write(headers_mut);
  220. res
  221. }
  222. Err(err) => {
  223. tracing::error!("Failed to render ssr: {}", err);
  224. Response::builder()
  225. .status(500)
  226. .body("Failed to render ssr".into())
  227. .unwrap()
  228. }
  229. }
  230. }
  231. })
  232. }
  233. /// An extractor for the request parts (used in [DioxusServerContext]). This will extract the method, uri, query, and headers from the request.
  234. pub fn request_parts(
  235. ) -> impl Filter<Extract = (http::request::Parts,), Error = warp::reject::Rejection> + Clone {
  236. warp::method()
  237. .and(warp::filters::path::full())
  238. .and(
  239. warp::filters::query::raw()
  240. .or(warp::any().map(String::new))
  241. .unify(),
  242. )
  243. .and(warp::header::headers_cloned())
  244. .and_then(move |method, path: FullPath, query, headers| async move {
  245. http::uri::Builder::new()
  246. .path_and_query(format!("{}?{}", path.as_str(), query))
  247. .build()
  248. .map_err(|err| {
  249. warp::reject::custom(FailedToReadBody(format!("Failed to build uri: {}", err)))
  250. })
  251. .map(|uri| {
  252. let mut req = http::Request::builder()
  253. .method(method)
  254. .uri(uri)
  255. .body(())
  256. .unwrap();
  257. req.headers_mut().extend(headers);
  258. req.into_parts().0
  259. })
  260. })
  261. }
  262. fn with_ssr_state<P: Clone + serde::Serialize + Send + Sync + 'static>(
  263. cfg: &ServeConfig<P>,
  264. ) -> impl Filter<Extract = (SSRState,), Error = std::convert::Infallible> + Clone {
  265. let renderer = SSRState::new(cfg);
  266. warp::any().map(move || renderer.clone())
  267. }
  268. #[derive(Debug)]
  269. struct FailedToReadBody(String);
  270. impl warp::reject::Reject for FailedToReadBody {}
  271. #[derive(Debug)]
  272. struct RecieveFailed(String);
  273. impl warp::reject::Reject for RecieveFailed {}
  274. /// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`].
  275. ///
  276. /// # Example
  277. /// ```rust
  278. /// #![allow(non_snake_case)]
  279. /// use dioxus_fullstack::prelude::*;
  280. ///
  281. /// #[tokio::main]
  282. /// async fn main() {
  283. /// let routes = connect_hot_reload();
  284. /// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
  285. /// }
  286. /// ```
  287. pub fn connect_hot_reload() -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone
  288. {
  289. #[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))]
  290. {
  291. warp::path!("_dioxus" / "hot_reload")
  292. .map(warp::reply)
  293. .map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND))
  294. }
  295. #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
  296. {
  297. use crate::hot_reload::HotReloadState;
  298. use futures_util::sink::SinkExt;
  299. use futures_util::StreamExt;
  300. use warp::ws::Message;
  301. let hot_reload = warp::path!("_dioxus" / "hot_reload")
  302. .and(warp::any().then(crate::hot_reload::spawn_hot_reload))
  303. .and(warp::ws())
  304. .map(move |state: &'static HotReloadState, ws: warp::ws::Ws| {
  305. #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
  306. ws.on_upgrade(move |mut websocket| {
  307. async move {
  308. println!("🔥 Hot Reload WebSocket connected");
  309. {
  310. // update any rsx calls that changed before the websocket connected.
  311. {
  312. println!("🔮 Finding updates since last compile...");
  313. let templates_read = state.templates.read().await;
  314. for template in &*templates_read {
  315. if websocket
  316. .send(Message::text(
  317. serde_json::to_string(&template).unwrap(),
  318. ))
  319. .await
  320. .is_err()
  321. {
  322. return;
  323. }
  324. }
  325. }
  326. println!("finished");
  327. }
  328. let mut rx = tokio_stream::wrappers::WatchStream::from_changes(
  329. state.message_receiver.clone(),
  330. );
  331. while let Some(change) = rx.next().await {
  332. if let Some(template) = change {
  333. let template = { serde_json::to_string(&template).unwrap() };
  334. if websocket.send(Message::text(template)).await.is_err() {
  335. break;
  336. };
  337. }
  338. }
  339. }
  340. })
  341. });
  342. let disconnect =
  343. warp::path!("_dioxus" / "disconnect")
  344. .and(warp::ws())
  345. .map(move |ws: warp::ws::Ws| {
  346. println!("disconnect");
  347. #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
  348. ws.on_upgrade(move |mut websocket| async move {
  349. struct DisconnectOnDrop(Option<warp::ws::WebSocket>);
  350. impl Drop for DisconnectOnDrop {
  351. fn drop(&mut self) {
  352. std::mem::drop(self.0.take().unwrap().close());
  353. }
  354. }
  355. let _ = websocket.send(Message::text("connected")).await;
  356. let mut ws = DisconnectOnDrop(Some(websocket));
  357. loop {
  358. if ws.0.as_mut().unwrap().next().await.is_none() {
  359. break;
  360. }
  361. }
  362. })
  363. });
  364. disconnect.or(hot_reload)
  365. }
  366. }