leptos_axum/lib.rs
1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3#![allow(clippy::type_complexity)]
4
5//! Provides functions to easily integrate Leptos with Axum.
6//!
7//! ## JS Fetch Integration
8//! The `leptos_axum` integration supports running in JavaScript-hosted WebAssembly
9//! runtimes, e.g., running inside Deno, Cloudflare Workers, or other JS environments.
10//! To run in this environment, you need to disable the default feature set and enable
11//! the `wasm` feature on `leptos_axum` in your `Cargo.toml`.
12//! ```toml
13//! leptos_axum = { version = "0.6.0", default-features = false, features = ["wasm"] }
14//! ```
15//!
16//! ## Features
17//! - `default`: supports running in a typical native Tokio/Axum environment
18//! - `wasm`: with `default-features = false`, supports running in a JS Fetch-based
19//! environment
20//!
21//! ### Important Note
22//! Prior to 0.5, using `default-features = false` on `leptos_axum` simply did nothing. Now, it actively
23//! disables features necessary to support the normal native/Tokio runtime environment we create. This can
24//! generate errors like the following, which don’t point to an obvious culprit:
25//! `
26//! `spawn_local` called from outside of a `task::LocalSet`
27//! `
28//! If you are not using the `wasm` feature, do not set `default-features = false` on this package.
29//!
30//!
31//! ## More information
32//!
33//! For more details on how to use the integrations, see the
34//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
35//! directory in the Leptos repository.
36
37#[cfg(feature = "default")]
38use axum::http::Uri;
39use axum::{
40 body::{Body, Bytes},
41 extract::{FromRef, FromRequestParts, MatchedPath, State},
42 http::{
43 header::{self, HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER},
44 request::Parts,
45 HeaderMap, Method, Request, Response, StatusCode,
46 },
47 response::IntoResponse,
48 routing::{delete, get, patch, post, put},
49};
50use futures::{stream::once, Future, Stream, StreamExt};
51use hydration_context::SsrSharedContext;
52use leptos::{
53 config::LeptosOptions,
54 context::{provide_context, use_context},
55 prelude::*,
56 reactive::{computed::ScopedFuture, owner::Owner},
57 IntoView,
58};
59use leptos_integration_utils::{
60 BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
61};
62use leptos_meta::ServerMetaContext;
63#[cfg(feature = "default")]
64use leptos_router::static_routes::ResolvedStaticPath;
65use leptos_router::{
66 components::provide_server_redirect, location::RequestUrl,
67 static_routes::RegenerationFn, ExpandOptionals, PathSegment, RouteList,
68 RouteListing, SsrMode,
69};
70use or_poisoned::OrPoisoned;
71use server_fn::{error::ServerFnErrorErr, redirect::REDIRECT_HEADER};
72#[cfg(feature = "default")]
73use std::sync::LazyLock;
74#[cfg(feature = "default")]
75use std::{collections::HashMap, path::Path};
76use std::{
77 collections::HashSet,
78 fmt::Debug,
79 io,
80 pin::Pin,
81 sync::{Arc, RwLock},
82};
83#[cfg(feature = "default")]
84use tower::util::ServiceExt;
85#[cfg(feature = "default")]
86use tower_http::services::ServeDir;
87// use tracing::Instrument; // TODO check tracing span -- was this used in 0.6 for a missing link?
88
89#[cfg(feature = "default")]
90mod service;
91#[cfg(feature = "default")]
92pub use service::ErrorHandler;
93
94/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
95/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
96#[derive(Debug, Clone, Default)]
97pub struct ResponseParts {
98 /// If provided, this will overwrite any other status code for this response.
99 pub status: Option<StatusCode>,
100 /// The map of headers that should be added to the response.
101 pub headers: HeaderMap,
102}
103
104impl ResponseParts {
105 /// Insert a header, overwriting any previous value with the same key
106 pub fn insert_header(&mut self, key: HeaderName, value: HeaderValue) {
107 self.headers.insert(key, value);
108 }
109 /// Append a header, leaving any header with the same key intact
110 pub fn append_header(&mut self, key: HeaderName, value: HeaderValue) {
111 self.headers.append(key, value);
112 }
113}
114
115/// Allows you to override details of the HTTP response like the status code and add Headers/Cookies.
116///
117/// `ResponseOptions` is provided via context when you use most of the handlers provided in this
118/// crate, including [`.leptos_routes`](LeptosRoutes::leptos_routes),
119/// [`.leptos_routes_with_context`](LeptosRoutes::leptos_routes_with_context), [`handle_server_fns`], etc.
120/// You can find the full set of provided context types in each handler function.
121///
122/// If you provide your own handler, you will need to provide `ResponseOptions` via context
123/// yourself if you want to access it via context.
124/// ```
125/// use leptos::prelude::*;
126///
127/// #[server]
128/// pub async fn get_opts() -> Result<(), ServerFnError> {
129/// let opts = expect_context::<leptos_axum::ResponseOptions>();
130/// Ok(())
131/// }
132#[derive(Debug, Clone, Default)]
133pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
134
135impl ResponseOptions {
136 /// A simpler way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`.
137 pub fn overwrite(&self, parts: ResponseParts) {
138 let mut writable = self.0.write().or_poisoned();
139 *writable = parts
140 }
141 /// Set the status of the returned Response.
142 pub fn set_status(&self, status: StatusCode) {
143 let mut writeable = self.0.write().or_poisoned();
144 let res_parts = &mut *writeable;
145 res_parts.status = Some(status);
146 }
147 /// Insert a header, overwriting any previous value with the same key.
148 pub fn insert_header(&self, key: HeaderName, value: HeaderValue) {
149 let mut writeable = self.0.write().or_poisoned();
150 let res_parts = &mut *writeable;
151 res_parts.headers.insert(key, value);
152 }
153 /// Append a header, leaving any header with the same key intact.
154 pub fn append_header(&self, key: HeaderName, value: HeaderValue) {
155 let mut writeable = self.0.write().or_poisoned();
156 let res_parts = &mut *writeable;
157 res_parts.headers.append(key, value);
158 }
159}
160
161struct AxumResponse(Response<Body>);
162
163impl ExtendResponse for AxumResponse {
164 type ResponseOptions = ResponseOptions;
165
166 fn from_stream(
167 stream: impl Stream<Item = String> + Send + 'static,
168 ) -> Self {
169 AxumResponse(
170 Body::from_stream(
171 stream.map(|chunk| Ok(chunk) as Result<String, std::io::Error>),
172 )
173 .into_response(),
174 )
175 }
176
177 fn extend_response(&mut self, res_options: &Self::ResponseOptions) {
178 let mut res_options = res_options.0.write().or_poisoned();
179 if let Some(status) = res_options.status {
180 *self.0.status_mut() = status;
181 }
182 self.0
183 .headers_mut()
184 .extend(std::mem::take(&mut res_options.headers));
185 }
186
187 fn set_default_content_type(&mut self, content_type: &str) {
188 let headers = self.0.headers_mut();
189 if !headers.contains_key(header::CONTENT_TYPE) {
190 // Set the Content Type headers on all responses. This makes Firefox show the page source
191 // without complaining
192 headers.insert(
193 header::CONTENT_TYPE,
194 HeaderValue::from_str(content_type).unwrap(),
195 );
196 }
197 }
198}
199
200/// Provides an easy way to redirect the user from within a server function.
201///
202/// Calling `redirect` in a server function will redirect the browser in three
203/// situations:
204/// 1. A server function that is calling in a [blocking
205/// resource](leptos::server::Resource::new_blocking).
206/// 2. A server function that is called from WASM running in the client (e.g., a dispatched action
207/// or a spawned `Future`).
208/// 3. A `<form>` submitted to the server function endpoint using default browser APIs (often due
209/// to using [`ActionForm`] without JS/WASM present.)
210///
211/// Using it with a non-blocking [`Resource`] will not work if you are using streaming rendering,
212/// as the response's headers will already have been sent by the time the server function calls `redirect()`.
213///
214/// ### Implementation
215///
216/// This sets the `Location` header to the URL given.
217///
218/// If the route or server function in which this is called is being accessed
219/// by an ordinary `GET` request or an HTML `<form>` without any enhancement, it also sets a
220/// status code of `302` for a temporary redirect. (This is determined by whether the `Accept`
221/// header contains `text/html` as it does for an ordinary navigation.)
222///
223/// Otherwise, it sets a custom header that indicates to the client that it should redirect,
224/// without actually setting the status code. This means that the client will not follow the
225/// redirect, and can therefore return the value of the server function and then handle
226/// the redirect with client-side routing.
227pub fn redirect(path: &str) {
228 if let (Some(req), Some(res)) =
229 (use_context::<Parts>(), use_context::<ResponseOptions>())
230 {
231 // insert the Location header in any case
232 res.insert_header(
233 header::LOCATION,
234 header::HeaderValue::from_str(path)
235 .expect("Failed to create HeaderValue"),
236 );
237
238 let accepts_html = req
239 .headers
240 .get(ACCEPT)
241 .and_then(|v| v.to_str().ok())
242 .map(|v| v.contains("text/html"))
243 .unwrap_or(false);
244 if accepts_html {
245 // if the request accepts text/html, it's a plain form request and needs
246 // to have the 302 code set
247 res.set_status(StatusCode::FOUND);
248 } else {
249 // otherwise, we sent it from the server fn client and actually don't want
250 // to set a real redirect, as this will break the ability to return data
251 // instead, set the REDIRECT_HEADER to indicate that the client should redirect
252 res.insert_header(
253 HeaderName::from_static(REDIRECT_HEADER),
254 HeaderValue::from_str("").unwrap(),
255 );
256 }
257 } else {
258 #[cfg(feature = "tracing")]
259 {
260 tracing::warn!(
261 "Couldn't retrieve either Parts or ResponseOptions while \
262 trying to redirect()."
263 );
264 }
265 #[cfg(not(feature = "tracing"))]
266 {
267 eprintln!(
268 "Couldn't retrieve either Parts or ResponseOptions while \
269 trying to redirect()."
270 );
271 }
272 }
273}
274
275/// Decomposes an HTTP request into its parts, allowing you to read its headers
276/// and other data without consuming the body. Creates a new Request from the
277/// original parts for further processing
278pub fn generate_request_and_parts(
279 req: Request<Body>,
280) -> (Request<Body>, Parts) {
281 let (parts, body) = req.into_parts();
282 let parts2 = parts.clone();
283 (Request::from_parts(parts, body), parts2)
284}
285
286/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
287/// run the server function if found, and return the resulting [`Response`].
288///
289/// This can then be set up at an appropriate route in your application:
290///
291/// ```no_run
292/// use axum::{handler::Handler, routing::post, Router};
293/// use leptos::prelude::*;
294/// use std::net::SocketAddr;
295///
296/// #[cfg(feature = "default")]
297/// #[tokio::main]
298/// async fn main() {
299/// let addr = SocketAddr::from(([127, 0, 0, 1], 8082));
300///
301/// // build our application with a route
302/// let app = Router::new()
303/// .route("/api/*fn_name", post(leptos_axum::handle_server_fns));
304///
305/// // run our app with hyper
306/// let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
307/// axum::serve(listener, app.into_make_service())
308/// .await
309/// .unwrap();
310/// }
311///
312/// # #[cfg(not(feature = "default"))]
313/// # fn main() { }
314/// ```
315/// Leptos provides a generic implementation of `handle_server_fns`. If access to more specific parts of the Request is desired,
316/// you can specify your own server fn handler based on this one and give it it's own route in the server macro.
317///
318/// ## Provided Context Types
319/// This function always provides context values including the following types:
320/// - [`Parts`]
321/// - [`ResponseOptions`]
322#[cfg_attr(
323 feature = "tracing",
324 tracing::instrument(level = "trace", fields(error), skip_all)
325)]
326pub async fn handle_server_fns(req: Request<Body>) -> impl IntoResponse {
327 handle_server_fns_inner(|| {}, req).await
328}
329
330fn init_executor() {
331 #[cfg(feature = "wasm")]
332 let _ = any_spawner::Executor::init_wasm_bindgen();
333 #[cfg(all(not(feature = "wasm"), feature = "default"))]
334 let _ = any_spawner::Executor::init_tokio();
335 #[cfg(all(not(feature = "wasm"), not(feature = "default")))]
336 {
337 eprintln!(
338 "It appears you have set 'default-features = false' on \
339 'leptos_axum', but are not using the 'wasm' feature. Either \
340 remove 'default-features = false' or, if you are running in a \
341 JS-hosted WASM server environment, add the 'wasm' feature."
342 );
343 }
344}
345
346/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
347/// run the server function if found, and return the resulting [`Response`].
348///
349/// This can then be set up at an appropriate route in your application:
350///
351/// This version allows you to pass in a closure to capture additional data from the layers above leptos
352/// and store it in context. To use it, you'll need to define your own route, and a handler function
353/// that takes in the data you'd like. See the [render_app_to_stream_with_context] docs for an example
354/// of one that should work much like this one.
355///
356/// **NOTE**: If your server functions expect a context, make sure to provide it both in
357/// [`handle_server_fns_with_context`] **and** in
358/// [`leptos_routes_with_context`](LeptosRoutes::leptos_routes_with_context) (or whatever
359/// rendering method you are using). During SSR, server functions are called by the rendering
360/// method, while subsequent calls from the client are handled by the server function handler.
361/// The same context needs to be provided to both handlers.
362///
363/// ## Provided Context Types
364/// This function always provides context values including the following types:
365/// - [`Parts`]
366/// - [`ResponseOptions`]
367#[cfg_attr(
368 feature = "tracing",
369 tracing::instrument(level = "trace", fields(error), skip_all)
370)]
371pub async fn handle_server_fns_with_context(
372 additional_context: impl Fn() + 'static + Clone + Send,
373 req: Request<Body>,
374) -> impl IntoResponse {
375 handle_server_fns_inner(additional_context, req).await
376}
377
378async fn handle_server_fns_inner(
379 additional_context: impl Fn() + 'static + Clone + Send,
380 req: Request<Body>,
381) -> impl IntoResponse {
382 let method = req.method().clone();
383 let path = req.uri().path().to_string();
384 let (req, parts) = generate_request_and_parts(req);
385
386 if let Some(mut service) =
387 server_fn::axum::get_server_fn_service(&path, method)
388 {
389 let owner = Owner::new();
390 owner
391 .with(|| {
392 ScopedFuture::new(async move {
393 provide_context(parts);
394 let res_options = ResponseOptions::default();
395 provide_context(res_options.clone());
396 additional_context();
397
398 // store Accepts and Referer in case we need them for redirect (below)
399 let accepts_html = req
400 .headers()
401 .get(ACCEPT)
402 .and_then(|v| v.to_str().ok())
403 .map(|v| v.contains("text/html"))
404 .unwrap_or(false);
405 let referrer = req.headers().get(REFERER).cloned();
406
407 // actually run the server fn
408 let mut res = AxumResponse(service.run(req).await);
409
410 // if it accepts text/html (i.e., is a plain form post) and doesn't already have a
411 // Location set, then redirect to the Referer
412 if accepts_html {
413 if let Some(referrer) = referrer {
414 let has_location =
415 res.0.headers().get(LOCATION).is_some();
416 if !has_location {
417 *res.0.status_mut() = StatusCode::FOUND;
418 res.0.headers_mut().insert(LOCATION, referrer);
419 }
420 }
421 }
422
423 // apply status code and headers if user changed them
424 res.extend_response(&res_options);
425 Ok(res.0)
426 })
427 })
428 .await
429 } else {
430 Response::builder()
431 .status(StatusCode::BAD_REQUEST)
432 .body(Body::from(format!(
433 "Could not find a server function at the route {path}. \
434 \n\nIt's likely that either
435 1. The API prefix you specify in the `#[server]` \
436 macro doesn't match the prefix at which your server function \
437 handler is mounted, or \n2. You are on a platform that \
438 doesn't support automatic server function registration and \
439 you need to call ServerFn::register_explicit() on the server \
440 function type, somewhere in your `main` function.",
441 )))
442 }
443 .expect("could not build Response")
444}
445
446/// A stream of bytes of HTML.
447pub type PinnedHtmlStream =
448 Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>;
449
450/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
451/// to route it using [leptos_router], serving an HTML stream of your application.
452///
453/// This can then be set up at an appropriate route in your application:
454/// ```no_run
455/// use axum::{handler::Handler, Router};
456/// use leptos::{config::get_configuration, prelude::*};
457/// use std::{env, net::SocketAddr};
458///
459/// #[component]
460/// fn MyApp() -> impl IntoView {
461/// view! { <main>"Hello, world!"</main> }
462/// }
463///
464/// #[cfg(feature = "default")]
465/// #[tokio::main]
466/// async fn main() {
467/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
468/// let leptos_options = conf.leptos_options;
469/// let addr = leptos_options.site_addr.clone();
470///
471/// // build our application with a route
472/// let app = Router::new().fallback(leptos_axum::render_app_to_stream(
473/// || { /* your application here */ },
474/// ));
475///
476/// // run our app with hyper
477/// let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
478/// axum::serve(listener, app.into_make_service())
479/// .await
480/// .unwrap();
481/// }
482///
483/// # #[cfg(not(feature = "default"))]
484/// # fn main() { }
485/// ```
486///
487/// ## Provided Context Types
488/// This function always provides context values including the following types:
489/// - [`Parts`]
490/// - [`ResponseOptions`]
491/// - [`ServerMetaContext`]
492#[cfg_attr(
493 feature = "tracing",
494 tracing::instrument(level = "trace", fields(error), skip_all)
495)]
496pub fn render_app_to_stream<IV>(
497 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
498) -> impl Fn(
499 Request<Body>,
500) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
501 + Clone
502 + Send
503 + 'static
504where
505 IV: IntoView + 'static,
506{
507 render_app_to_stream_with_context(|| {}, app_fn)
508}
509
510/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
511/// to route it using [leptos_router], serving an HTML stream of your application.
512/// The difference between calling this and `render_app_to_stream_with_context()` is that this
513/// one respects the `SsrMode` on each Route and thus requires `Vec<AxumRouteListing>` for route checking.
514/// This is useful if you are using `.leptos_routes_with_handler()`
515#[cfg_attr(
516 feature = "tracing",
517 tracing::instrument(level = "trace", fields(error), skip_all)
518)]
519pub fn render_route<S, IV>(
520 paths: Vec<AxumRouteListing>,
521 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
522) -> impl Fn(
523 State<S>,
524 Request<Body>,
525) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
526 + Clone
527 + Send
528 + 'static
529where
530 IV: IntoView + 'static,
531 LeptosOptions: FromRef<S>,
532 S: Send + 'static,
533{
534 render_route_with_context(paths, || {}, app_fn)
535}
536
537/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
538/// to route it using [leptos_router], serving an in-order HTML stream of your application.
539/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
540/// sending down its HTML. The app will become interactive once it has fully loaded.
541///
542/// This can then be set up at an appropriate route in your application:
543/// ```no_run
544/// use axum::{handler::Handler, Router};
545/// use leptos::{config::get_configuration, prelude::*};
546/// use std::{env, net::SocketAddr};
547///
548/// #[component]
549/// fn MyApp() -> impl IntoView {
550/// view! { <main>"Hello, world!"</main> }
551/// }
552///
553/// #[cfg(feature = "default")]
554/// #[tokio::main]
555/// async fn main() {
556/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
557/// let leptos_options = conf.leptos_options;
558/// let addr = leptos_options.site_addr.clone();
559///
560/// // build our application with a route
561/// let app = Router::new().fallback(
562/// leptos_axum::render_app_to_stream_in_order(|| view! { <MyApp/> }),
563/// );
564///
565/// // run our app with hyper
566/// let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
567/// axum::serve(listener, app.into_make_service())
568/// .await
569/// .unwrap();
570/// }
571///
572/// # #[cfg(not(feature = "default"))]
573/// # fn main() { }
574/// ```
575///
576/// ## Provided Context Types
577/// This function always provides context values including the following types:
578/// - [`Parts`]
579/// - [`ResponseOptions`]
580/// - [`ServerMetaContext`]
581#[cfg_attr(
582 feature = "tracing",
583 tracing::instrument(level = "trace", fields(error), skip_all)
584)]
585pub fn render_app_to_stream_in_order<IV>(
586 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
587) -> impl Fn(
588 Request<Body>,
589) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
590 + Clone
591 + Send
592 + 'static
593where
594 IV: IntoView + 'static,
595{
596 render_app_to_stream_in_order_with_context(|| {}, app_fn)
597}
598
599/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
600/// to route it using [leptos_router], serving an HTML stream of your application.
601///
602/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
603/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
604/// the data to leptos in a closure. An example is below
605/// ```
606/// use axum::{
607/// body::Body,
608/// extract::Path,
609/// http::Request,
610/// response::{IntoResponse, Response},
611/// };
612/// use leptos::{config::LeptosOptions, context::provide_context, prelude::*};
613///
614/// async fn custom_handler(
615/// Path(id): Path<String>,
616/// req: Request<Body>,
617/// ) -> Response {
618/// let handler = leptos_axum::render_app_to_stream_with_context(
619/// move || {
620/// provide_context(id.clone());
621/// },
622/// || { /* your app here */ },
623/// );
624/// handler(req).await.into_response()
625/// }
626/// ```
627/// Otherwise, this function is identical to [render_app_to_stream].
628///
629/// ## Provided Context Types
630/// This function always provides context values including the following types:
631/// - [`Parts`]
632/// - [`ResponseOptions`]
633/// - [`ServerMetaContext`]
634#[cfg_attr(
635 feature = "tracing",
636 tracing::instrument(level = "trace", fields(error), skip_all)
637)]
638pub fn render_app_to_stream_with_context<IV>(
639 additional_context: impl Fn() + 'static + Clone + Send + Sync,
640 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
641) -> impl Fn(
642 Request<Body>,
643) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
644 + Clone
645 + Send
646 + Sync
647 + 'static
648where
649 IV: IntoView + 'static,
650{
651 render_app_to_stream_with_context_and_replace_blocks(
652 additional_context,
653 app_fn,
654 false,
655 )
656}
657/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
658/// to route it using [leptos_router], serving an HTML stream of your application. It allows you
659/// to pass in a context function with additional info to be made available to the app
660/// The difference between calling this and `render_app_to_stream_with_context()` is that this
661/// one respects the `SsrMode` on each Route, and thus requires `Vec<AxumRouteListing>` for route checking.
662/// This is useful if you are using `.leptos_routes_with_handler()`.
663#[cfg_attr(
664 feature = "tracing",
665 tracing::instrument(level = "trace", fields(error), skip_all)
666)]
667pub fn render_route_with_context<S, IV>(
668 paths: Vec<AxumRouteListing>,
669 additional_context: impl Fn() + 'static + Clone + Send + Sync,
670 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
671) -> impl Fn(
672 State<S>,
673 Request<Body>,
674) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
675 + Clone
676 + Send
677 + 'static
678where
679 IV: IntoView + 'static,
680 LeptosOptions: FromRef<S>,
681 S: Send + 'static,
682{
683 let ooo = render_app_to_stream_with_context(
684 additional_context.clone(),
685 app_fn.clone(),
686 );
687 let pb = render_app_to_stream_with_context_and_replace_blocks(
688 additional_context.clone(),
689 app_fn.clone(),
690 true,
691 );
692 let io = render_app_to_stream_in_order_with_context(
693 additional_context.clone(),
694 app_fn.clone(),
695 );
696 let asyn = render_app_async_stream_with_context(
697 additional_context.clone(),
698 app_fn.clone(),
699 );
700
701 move |state, req| {
702 // 1. Process route to match the values in routeListing
703 let path = req
704 .extensions()
705 .get::<MatchedPath>()
706 .expect("Failed to get Axum router rule")
707 .as_str();
708 // 2. Find RouteListing in paths. This should probably be optimized, we probably don't want to
709 // search for this every time
710 let listing: &AxumRouteListing =
711 paths.iter().find(|r| r.path() == path).unwrap_or_else(|| {
712 panic!(
713 "Failed to find the route {path} requested by the user. \
714 This suggests that the routing rules in the Router that \
715 call this handler needs to be edited!"
716 )
717 });
718 // 3. Match listing mode against known, and choose function
719 match listing.mode() {
720 SsrMode::OutOfOrder => ooo(req),
721 SsrMode::PartiallyBlocked => pb(req),
722 SsrMode::InOrder => io(req),
723 SsrMode::Async => asyn(req),
724 SsrMode::Static(_) => {
725 #[cfg(feature = "default")]
726 {
727 let regenerate = listing.regenerate.clone();
728 handle_static_route(
729 additional_context.clone(),
730 app_fn.clone(),
731 regenerate,
732 )(state, req)
733 }
734 #[cfg(not(feature = "default"))]
735 {
736 _ = state;
737 panic!(
738 "Static routes are not currently supported on WASM32 \
739 server targets."
740 );
741 }
742 }
743 }
744 }
745}
746
747/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
748/// to route it using [leptos_router], serving an HTML stream of your application.
749///
750/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
751/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
752/// the data to leptos in a closure.
753///
754/// `replace_blocks` additionally lets you specify whether `<Suspense/>` fragments that read
755/// from blocking resources should be retrojected into the HTML that's initially served, rather
756/// than dynamically inserting them with JavaScript on the client. This means you will have
757/// better support if JavaScript is not enabled, in exchange for a marginally slower response time.
758///
759/// Otherwise, this function is identical to [render_app_to_stream_with_context].
760///
761/// ## Provided Context Types
762/// This function always provides context values including the following types:
763/// - [`Parts`]
764/// - [`ResponseOptions`]
765/// - [`ServerMetaContext`]
766#[cfg_attr(
767 feature = "tracing",
768 tracing::instrument(level = "trace", fields(error), skip_all)
769)]
770pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
771 additional_context: impl Fn() + 'static + Clone + Send + Sync,
772 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
773 replace_blocks: bool,
774) -> impl Fn(
775 Request<Body>,
776) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
777 + Clone
778 + Send
779 + Sync
780 + 'static
781where
782 IV: IntoView + 'static,
783{
784 _ = replace_blocks; // TODO
785 handle_response(additional_context, app_fn, |app, chunks, supports_ooo| {
786 Box::pin(async move {
787 let app = if cfg!(feature = "islands-router") {
788 if supports_ooo {
789 app.to_html_stream_out_of_order_branching()
790 } else {
791 app.to_html_stream_in_order_branching()
792 }
793 } else if supports_ooo {
794 app.to_html_stream_out_of_order()
795 } else {
796 app.to_html_stream_in_order()
797 };
798 Box::pin(app.chain(chunks())) as PinnedStream<String>
799 })
800 })
801}
802
803/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
804/// to route it using [leptos_router], serving an in-order HTML stream of your application.
805/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
806/// sending down its HTML. The app will become interactive once it has fully loaded.
807///
808/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
809/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
810/// the data to leptos in a closure. An example is below
811/// ```
812/// use axum::{
813/// body::Body,
814/// extract::Path,
815/// http::Request,
816/// response::{IntoResponse, Response},
817/// };
818/// use leptos::context::provide_context;
819///
820/// async fn custom_handler(
821/// Path(id): Path<String>,
822/// req: Request<Body>,
823/// ) -> Response {
824/// let handler = leptos_axum::render_app_to_stream_in_order_with_context(
825/// move || {
826/// provide_context(id.clone());
827/// },
828/// || { /* your application here */ },
829/// );
830/// handler(req).await.into_response()
831/// }
832/// ```
833/// Otherwise, this function is identical to [render_app_to_stream].
834///
835/// ## Provided Context Types
836/// This function always provides context values including the following types:
837/// - [`Parts`]
838/// - [`ResponseOptions`]
839/// - [`ServerMetaContext`]
840#[cfg_attr(
841 feature = "tracing",
842 tracing::instrument(level = "trace", fields(error), skip_all)
843)]
844pub fn render_app_to_stream_in_order_with_context<IV>(
845 additional_context: impl Fn() + 'static + Clone + Send + Sync,
846 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
847) -> impl Fn(
848 Request<Body>,
849) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
850 + Clone
851 + Send
852 + 'static
853where
854 IV: IntoView + 'static,
855{
856 handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
857 let app = if cfg!(feature = "islands-router") {
858 app.to_html_stream_in_order_branching()
859 } else {
860 app.to_html_stream_in_order()
861 };
862 Box::pin(async move {
863 Box::pin(app.chain(chunks())) as PinnedStream<String>
864 })
865 })
866}
867
868fn handle_response<IV>(
869 additional_context: impl Fn() + 'static + Clone + Send + Sync,
870 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
871 stream_builder: fn(
872 IV,
873 BoxedFnOnce<PinnedStream<String>>,
874 bool,
875 ) -> PinnedFuture<PinnedStream<String>>,
876) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>>
877 + Clone
878 + Send
879 + Sync
880 + 'static
881where
882 IV: IntoView + 'static,
883{
884 move |req: Request<Body>| {
885 let app_fn = app_fn.clone();
886 let additional_context = additional_context.clone();
887 handle_response_inner(additional_context, app_fn, req, stream_builder)
888 }
889}
890
891/// Can be used in conjunction with a custom [file_and_error_handler_with_context] to process an Axum [Request](axum::extract::Request) into an Axum [Response](axum::response::Response)
892pub fn handle_response_inner<IV>(
893 additional_context: impl Fn() + 'static + Clone + Send,
894 app_fn: impl FnOnce() -> IV + Send + 'static,
895 req: Request<Body>,
896 stream_builder: fn(
897 IV,
898 BoxedFnOnce<PinnedStream<String>>,
899 bool,
900 ) -> PinnedFuture<PinnedStream<String>>,
901) -> PinnedFuture<Response<Body>>
902where
903 IV: IntoView + 'static,
904{
905 Box::pin(async move {
906 let is_island_router_navigation = cfg!(feature = "islands-router")
907 && req.headers().get("Islands-Router").is_some();
908
909 let add_context = additional_context.clone();
910 let res_options = ResponseOptions::default();
911 let (meta_context, meta_output) = ServerMetaContext::new();
912
913 let additional_context = {
914 let meta_context = meta_context.clone();
915 let res_options = res_options.clone();
916 move || {
917 // Need to get the path and query string of the Request
918 // For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
919 let path = req.uri().path_and_query().unwrap().as_str();
920
921 let full_path = format!("http://leptos.dev{path}");
922 let (_, req_parts) = generate_request_and_parts(req);
923 provide_contexts(
924 &full_path,
925 &meta_context,
926 req_parts,
927 res_options.clone(),
928 );
929 add_context();
930
931 if is_island_router_navigation {
932 provide_context(IslandsRouterNavigation);
933 }
934 }
935 };
936
937 let res = AxumResponse::from_app(
938 app_fn,
939 meta_output,
940 additional_context,
941 res_options,
942 stream_builder,
943 !is_island_router_navigation,
944 )
945 .await;
946
947 res.0
948 })
949}
950
951#[cfg_attr(
952 feature = "tracing",
953 tracing::instrument(level = "trace", fields(error), skip_all)
954)]
955fn provide_contexts(
956 path: &str,
957 meta_context: &ServerMetaContext,
958 parts: Parts,
959 default_res_options: ResponseOptions,
960) {
961 provide_context(RequestUrl::new(path));
962 provide_context(meta_context.clone());
963 provide_context(parts);
964 provide_context(default_res_options);
965 provide_server_redirect(redirect);
966 leptos::nonce::provide_nonce();
967}
968
969/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
970/// to route it using [leptos_router], asynchronously rendering an HTML page after all
971/// `async` resources have loaded.
972///
973/// This can then be set up at an appropriate route in your application:
974/// ```no_run
975/// use axum::{handler::Handler, Router};
976/// use leptos::{config::get_configuration, prelude::*};
977/// use std::{env, net::SocketAddr};
978///
979/// #[component]
980/// fn MyApp() -> impl IntoView {
981/// view! { <main>"Hello, world!"</main> }
982/// }
983///
984/// #[cfg(feature = "default")]
985/// #[tokio::main]
986/// async fn main() {
987/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
988/// let leptos_options = conf.leptos_options;
989/// let addr = leptos_options.site_addr.clone();
990///
991/// // build our application with a route
992/// let app = Router::new()
993/// .fallback(leptos_axum::render_app_async(|| view! { <MyApp/> }));
994///
995/// // run our app with hyper
996/// // `axum::Server` is a re-export of `hyper::Server`
997/// let listener =
998/// tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
999/// axum::serve(listener, app.into_make_service())
1000/// .await
1001/// .unwrap();
1002/// }
1003///
1004/// # #[cfg(not(feature = "default"))]
1005/// # fn main() { }
1006/// ```
1007///
1008/// ## Provided Context Types
1009/// This function always provides context values including the following types:
1010/// - [`Parts`]
1011/// - [`ResponseOptions`]
1012/// - [`ServerMetaContext`]
1013#[cfg_attr(
1014 feature = "tracing",
1015 tracing::instrument(level = "trace", fields(error), skip_all)
1016)]
1017pub fn render_app_async<IV>(
1018 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
1019) -> impl Fn(
1020 Request<Body>,
1021) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
1022 + Clone
1023 + Send
1024 + 'static
1025where
1026 IV: IntoView + 'static,
1027{
1028 render_app_async_with_context(|| {}, app_fn)
1029}
1030
1031/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
1032/// to route it using [leptos_router], asynchronously rendering an HTML page after all
1033/// `async` resources have loaded.
1034///
1035/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
1036/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
1037/// the data to leptos in a closure. An example is below
1038/// ```
1039/// use axum::{
1040/// body::Body,
1041/// extract::Path,
1042/// http::Request,
1043/// response::{IntoResponse, Response},
1044/// };
1045/// use leptos::context::provide_context;
1046///
1047/// async fn custom_handler(
1048/// Path(id): Path<String>,
1049/// req: Request<Body>,
1050/// ) -> Response {
1051/// let handler = leptos_axum::render_app_async_with_context(
1052/// move || {
1053/// provide_context(id.clone());
1054/// },
1055/// || { /* your application here */ },
1056/// );
1057/// handler(req).await.into_response()
1058/// }
1059/// ```
1060/// Otherwise, this function is identical to [render_app_to_stream].
1061///
1062/// ## Provided Context Types
1063/// This function always provides context values including the following types:
1064/// - [`Parts`]
1065/// - [`ResponseOptions`]
1066/// - [`ServerMetaContext`]
1067#[cfg_attr(
1068 feature = "tracing",
1069 tracing::instrument(level = "trace", fields(error), skip_all)
1070)]
1071pub fn render_app_async_stream_with_context<IV>(
1072 additional_context: impl Fn() + 'static + Clone + Send + Sync,
1073 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
1074) -> impl Fn(
1075 Request<Body>,
1076) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
1077 + Clone
1078 + Send
1079 + 'static
1080where
1081 IV: IntoView + 'static,
1082{
1083 handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
1084 Box::pin(async move {
1085 let app = if cfg!(feature = "islands-router") {
1086 app.to_html_stream_in_order_branching()
1087 } else {
1088 app.to_html_stream_in_order()
1089 };
1090 let app = app.collect::<String>().await;
1091 let chunks = chunks();
1092 Box::pin(once(async move { app }).chain(chunks))
1093 as PinnedStream<String>
1094 })
1095 })
1096}
1097
1098/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
1099/// to route it using [leptos_router], asynchronously rendering an HTML page after all
1100/// `async` resources have loaded.
1101///
1102/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
1103/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
1104/// the data to leptos in a closure. An example is below
1105/// ```
1106/// use axum::{
1107/// body::Body,
1108/// extract::Path,
1109/// http::Request,
1110/// response::{IntoResponse, Response},
1111/// };
1112/// use leptos::context::provide_context;
1113///
1114/// async fn custom_handler(
1115/// Path(id): Path<String>,
1116/// req: Request<Body>,
1117/// ) -> Response {
1118/// let handler = leptos_axum::render_app_async_with_context(
1119/// move || {
1120/// provide_context(id.clone());
1121/// },
1122/// || { /* your application here */ },
1123/// );
1124/// handler(req).await.into_response()
1125/// }
1126/// ```
1127/// Otherwise, this function is identical to [render_app_to_stream].
1128///
1129/// ## Provided Context Types
1130/// This function always provides context values including the following types:
1131/// - [`Parts`]
1132/// - [`ResponseOptions`]
1133/// - [`ServerMetaContext`]
1134#[cfg_attr(
1135 feature = "tracing",
1136 tracing::instrument(level = "trace", fields(error), skip_all)
1137)]
1138pub fn render_app_async_with_context<IV>(
1139 additional_context: impl Fn() + 'static + Clone + Send + Sync,
1140 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
1141) -> impl Fn(
1142 Request<Body>,
1143) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
1144 + Clone
1145 + Send
1146 + 'static
1147where
1148 IV: IntoView + 'static,
1149{
1150 handle_response(additional_context, app_fn, async_stream_builder)
1151}
1152
1153fn async_stream_builder<IV>(
1154 app: IV,
1155 chunks: BoxedFnOnce<PinnedStream<String>>,
1156 _supports_ooo: bool,
1157) -> PinnedFuture<PinnedStream<String>>
1158where
1159 IV: IntoView + 'static,
1160{
1161 Box::pin(async move {
1162 let app = if cfg!(feature = "islands-router") {
1163 app.to_html_stream_in_order_branching()
1164 } else {
1165 app.to_html_stream_in_order()
1166 };
1167 let app = app.collect::<String>().await;
1168 let chunks = chunks();
1169 Box::pin(once(async move { app }).chain(chunks)) as PinnedStream<String>
1170 })
1171}
1172
1173/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
1174/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
1175/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
1176#[cfg_attr(
1177 feature = "tracing",
1178 tracing::instrument(level = "trace", fields(error), skip_all)
1179)]
1180pub fn generate_route_list<IV>(
1181 app_fn: impl Fn() -> IV + 'static + Clone + Send,
1182) -> Vec<AxumRouteListing>
1183where
1184 IV: IntoView + 'static,
1185{
1186 generate_route_list_with_exclusions_and_ssg(app_fn, None).0
1187}
1188
1189/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
1190/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
1191/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
1192#[cfg_attr(
1193 feature = "tracing",
1194 tracing::instrument(level = "trace", fields(error), skip_all)
1195)]
1196pub fn generate_route_list_with_ssg<IV>(
1197 app_fn: impl Fn() -> IV + 'static + Clone + Send,
1198) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
1199where
1200 IV: IntoView + 'static,
1201{
1202 generate_route_list_with_exclusions_and_ssg(app_fn, None)
1203}
1204
1205/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
1206/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
1207/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
1208/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
1209#[cfg_attr(
1210 feature = "tracing",
1211 tracing::instrument(level = "trace", fields(error), skip_all)
1212)]
1213pub fn generate_route_list_with_exclusions<IV>(
1214 app_fn: impl Fn() -> IV + 'static + Clone + Send,
1215 excluded_routes: Option<Vec<String>>,
1216) -> Vec<AxumRouteListing>
1217where
1218 IV: IntoView + 'static,
1219{
1220 generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
1221}
1222
1223/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
1224/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
1225/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
1226/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
1227#[cfg_attr(
1228 feature = "tracing",
1229 tracing::instrument(level = "trace", fields(error), skip_all)
1230)]
1231pub fn generate_route_list_with_exclusions_and_ssg<IV>(
1232 app_fn: impl Fn() -> IV + 'static + Clone + Send,
1233 excluded_routes: Option<Vec<String>>,
1234) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
1235where
1236 IV: IntoView + 'static,
1237{
1238 generate_route_list_with_exclusions_and_ssg_and_context(
1239 app_fn,
1240 excluded_routes,
1241 || {},
1242 )
1243}
1244
1245#[derive(Clone, Debug, Default)]
1246/// A route that this application can serve.
1247pub struct AxumRouteListing {
1248 path: String,
1249 mode: SsrMode,
1250 methods: Vec<leptos_router::Method>,
1251 #[allow(unused)]
1252 regenerate: Vec<RegenerationFn>,
1253 exclude: bool,
1254}
1255
1256trait IntoRouteListing: Sized {
1257 fn into_route_listing(self) -> Vec<AxumRouteListing>;
1258}
1259
1260impl IntoRouteListing for RouteListing {
1261 fn into_route_listing(self) -> Vec<AxumRouteListing> {
1262 self.path()
1263 .to_vec()
1264 .expand_optionals()
1265 .into_iter()
1266 .map(|path| {
1267 let path = path.to_axum_path();
1268 let path = if path.is_empty() {
1269 "/".to_string()
1270 } else {
1271 path
1272 };
1273 let mode = self.mode();
1274 let methods = self.methods().collect();
1275 let regenerate = self.regenerate().into();
1276 AxumRouteListing {
1277 path,
1278 mode: mode.clone(),
1279 methods,
1280 regenerate,
1281 exclude: false,
1282 }
1283 })
1284 .collect()
1285 }
1286}
1287
1288impl AxumRouteListing {
1289 /// Create a route listing from its parts.
1290 pub fn new(
1291 path: String,
1292 mode: SsrMode,
1293 methods: impl IntoIterator<Item = leptos_router::Method>,
1294 regenerate: impl Into<Vec<RegenerationFn>>,
1295 ) -> Self {
1296 Self {
1297 path,
1298 mode,
1299 methods: methods.into_iter().collect(),
1300 regenerate: regenerate.into(),
1301 exclude: false,
1302 }
1303 }
1304
1305 /// The path this route handles.
1306 pub fn path(&self) -> &str {
1307 &self.path
1308 }
1309
1310 /// The rendering mode for this path.
1311 pub fn mode(&self) -> &SsrMode {
1312 &self.mode
1313 }
1314
1315 /// The HTTP request methods this path can handle.
1316 pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
1317 self.methods.iter().copied()
1318 }
1319}
1320
1321/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
1322/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
1323/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
1324/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
1325/// Additional context will be provided to the app Element.
1326#[cfg_attr(
1327 feature = "tracing",
1328 tracing::instrument(level = "trace", fields(error), skip_all)
1329)]
1330pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
1331 app_fn: impl Fn() -> IV + Clone + Send + 'static,
1332 excluded_routes: Option<Vec<String>>,
1333 additional_context: impl Fn() + Clone + Send + 'static,
1334) -> (Vec<AxumRouteListing>, StaticRouteGenerator)
1335where
1336 IV: IntoView + 'static,
1337{
1338 // do some basic reactive setup
1339 init_executor();
1340 let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
1341
1342 let routes = owner
1343 .with(|| {
1344 // stub out a path for now
1345 provide_context(RequestUrl::new(""));
1346 let (mock_parts, _) = Request::new(Body::from("")).into_parts();
1347 let (mock_meta, _) = ServerMetaContext::new();
1348 provide_contexts("", &mock_meta, mock_parts, Default::default());
1349 additional_context();
1350 RouteList::generate(&app_fn)
1351 })
1352 .unwrap_or_default();
1353
1354 let generator = StaticRouteGenerator::new(
1355 &routes,
1356 app_fn.clone(),
1357 additional_context.clone(),
1358 );
1359
1360 // Axum's Router defines Root routes as "/" not ""
1361 let mut routes = routes
1362 .into_inner()
1363 .into_iter()
1364 .flat_map(IntoRouteListing::into_route_listing)
1365 .collect::<Vec<_>>();
1366
1367 let routes = if routes.is_empty() {
1368 vec![AxumRouteListing::new(
1369 "/".to_string(),
1370 Default::default(),
1371 [leptos_router::Method::Get],
1372 vec![],
1373 )]
1374 } else {
1375 // Routes to exclude from auto generation
1376 if let Some(excluded_routes) = &excluded_routes {
1377 routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
1378 }
1379 routes
1380 };
1381 let excluded =
1382 excluded_routes
1383 .into_iter()
1384 .flatten()
1385 .map(|path| AxumRouteListing {
1386 path,
1387 mode: Default::default(),
1388 methods: Vec::new(),
1389 regenerate: Vec::new(),
1390 exclude: true,
1391 });
1392
1393 (routes.into_iter().chain(excluded).collect(), generator)
1394}
1395
1396/// Allows generating any prerendered routes.
1397#[allow(clippy::type_complexity)]
1398pub struct StaticRouteGenerator(
1399 // this is here to keep the root owner alive for the duration
1400 // of the route generation, so that base context provided continues
1401 // to exist until it is dropped
1402 #[allow(dead_code)] Owner,
1403 Box<dyn FnOnce(&LeptosOptions) -> PinnedFuture<()> + Send>,
1404);
1405
1406impl StaticRouteGenerator {
1407 #[cfg(feature = "default")]
1408 fn render_route<IV: IntoView + 'static>(
1409 path: String,
1410 app_fn: impl Fn() -> IV + Clone + Send + 'static,
1411 additional_context: impl Fn() + Clone + Send + 'static,
1412 ) -> impl Future<Output = (Owner, String)> {
1413 let (meta_context, meta_output) = ServerMetaContext::new();
1414 let additional_context = {
1415 let add_context = additional_context.clone();
1416 move || {
1417 let full_path = format!("http://leptos.dev{path}");
1418 let mock_req = Request::builder()
1419 .method(Method::GET)
1420 .header("Accept", "text/html")
1421 .body(Body::empty())
1422 .unwrap();
1423 let (mock_parts, _) = mock_req.into_parts();
1424 let res_options = ResponseOptions::default();
1425 provide_contexts(
1426 &full_path,
1427 &meta_context,
1428 mock_parts,
1429 res_options,
1430 );
1431 add_context();
1432 }
1433 };
1434
1435 let (owner, stream) = leptos_integration_utils::build_response(
1436 app_fn.clone(),
1437 additional_context,
1438 async_stream_builder,
1439 false,
1440 );
1441
1442 let sc = owner.shared_context().unwrap();
1443
1444 async move {
1445 let stream = stream.await;
1446 while let Some(pending) = sc.await_deferred() {
1447 pending.await;
1448 }
1449
1450 let html = meta_output
1451 .inject_meta_context(stream)
1452 .await
1453 .collect::<String>()
1454 .await;
1455 (owner, html)
1456 }
1457 }
1458
1459 /// Creates a new static route generator from the given list of route definitions.
1460 pub fn new<IV>(
1461 routes: &RouteList,
1462 app_fn: impl Fn() -> IV + Clone + Send + 'static,
1463 additional_context: impl Fn() + Clone + Send + 'static,
1464 ) -> Self
1465 where
1466 IV: IntoView + 'static,
1467 {
1468 #[cfg(feature = "default")]
1469 {
1470 let owner = Owner::new();
1471 Self(owner.clone(), {
1472 let routes = routes.clone();
1473 Box::new(move |options| {
1474 let options = options.clone();
1475 let app_fn = app_fn.clone();
1476 let additional_context = additional_context.clone();
1477 owner.with(|| {
1478 additional_context();
1479 Box::pin(ScopedFuture::new(routes.generate_static_files(
1480 move |path: &ResolvedStaticPath| {
1481 Self::render_route(
1482 path.to_string(),
1483 app_fn.clone(),
1484 additional_context.clone(),
1485 )
1486 },
1487 move |path: &ResolvedStaticPath,
1488 owner: &Owner,
1489 html: String| {
1490 let options = options.clone();
1491 let path = path.to_owned();
1492 let response_options = owner.with(use_context);
1493 async move {
1494 write_static_route(
1495 &options,
1496 response_options,
1497 path.as_ref(),
1498 &html,
1499 )
1500 .await
1501 }
1502 },
1503 was_404,
1504 )))
1505 })
1506 })
1507 })
1508 }
1509
1510 #[cfg(not(feature = "default"))]
1511 {
1512 _ = routes;
1513 _ = app_fn;
1514 _ = additional_context;
1515 Self(
1516 Owner::new(),
1517 Box::new(|_| {
1518 panic!(
1519 "Static routes are not currently supported on WASM32 \
1520 server targets."
1521 );
1522 }),
1523 )
1524 }
1525 }
1526
1527 /// Generates the routes.
1528 pub async fn generate(self, options: &LeptosOptions) {
1529 (self.1)(options).await
1530 }
1531}
1532
1533#[cfg(feature = "default")]
1534static STATIC_HEADERS: LazyLock<
1535 std::sync::RwLock<HashMap<String, ResponseOptions>>,
1536> = LazyLock::new(Default::default);
1537
1538#[cfg(feature = "default")]
1539fn was_404(owner: &Owner) -> bool {
1540 let resp = owner.with(|| expect_context::<ResponseOptions>());
1541 let status = resp.0.read().or_poisoned().status;
1542
1543 if let Some(status) = status {
1544 return status == StatusCode::NOT_FOUND;
1545 }
1546
1547 false
1548}
1549
1550#[cfg(feature = "default")]
1551fn static_path(options: &LeptosOptions, path: &str) -> String {
1552 use leptos_integration_utils::static_file_path;
1553
1554 // If the path ends with a trailing slash, we generate the path
1555 // as a directory with a index.html file inside.
1556 if path != "/" && path.ends_with("/") {
1557 static_file_path(options, &format!("{path}index"))
1558 } else {
1559 static_file_path(options, path)
1560 }
1561}
1562
1563#[cfg(feature = "default")]
1564async fn write_static_route(
1565 options: &LeptosOptions,
1566 response_options: Option<ResponseOptions>,
1567 path: &str,
1568 html: &str,
1569) -> Result<(), std::io::Error> {
1570 if let Some(options) = response_options {
1571 STATIC_HEADERS
1572 .write()
1573 .or_poisoned()
1574 .insert(path.to_string(), options);
1575 }
1576
1577 let path = static_path(options, path);
1578 let path = Path::new(&path);
1579 if let Some(path) = path.parent() {
1580 tokio::fs::create_dir_all(path).await?;
1581 }
1582 tokio::fs::write(path, &html).await?;
1583
1584 Ok(())
1585}
1586
1587#[cfg(feature = "default")]
1588fn handle_static_route<S, IV>(
1589 additional_context: impl Fn() + 'static + Clone + Send,
1590 app_fn: impl Fn() -> IV + Clone + Send + 'static,
1591 regenerate: Vec<RegenerationFn>,
1592) -> impl Fn(
1593 State<S>,
1594 Request<Body>,
1595) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
1596 + Clone
1597 + Send
1598 + 'static
1599where
1600 LeptosOptions: FromRef<S>,
1601 S: Send + 'static,
1602 IV: IntoView + 'static,
1603{
1604 use tower_http::services::ServeFile;
1605
1606 move |state, req| {
1607 let app_fn = app_fn.clone();
1608 let additional_context = additional_context.clone();
1609 let regenerate = regenerate.clone();
1610 Box::pin(async move {
1611 let options = LeptosOptions::from_ref(&state);
1612 let orig_path = req.uri().path();
1613 let path = static_path(&options, orig_path);
1614 let path = Path::new(&path);
1615 let exists = tokio::fs::try_exists(path).await.unwrap_or(false);
1616
1617 let (response_options, html) = if !exists {
1618 let path = ResolvedStaticPath::new(orig_path);
1619
1620 let (owner, html) = path
1621 .build(
1622 move |path: &ResolvedStaticPath| {
1623 StaticRouteGenerator::render_route(
1624 path.to_string(),
1625 app_fn.clone(),
1626 additional_context.clone(),
1627 )
1628 },
1629 move |path: &ResolvedStaticPath,
1630 owner: &Owner,
1631 html: String| {
1632 let options = options.clone();
1633 let path = path.to_owned();
1634 let response_options = owner.with(use_context);
1635 async move {
1636 write_static_route(
1637 &options,
1638 response_options,
1639 path.as_ref(),
1640 &html,
1641 )
1642 .await
1643 }
1644 },
1645 was_404,
1646 regenerate,
1647 )
1648 .await;
1649 (owner.with(use_context::<ResponseOptions>), html)
1650 } else {
1651 let headers =
1652 STATIC_HEADERS.read().or_poisoned().get(orig_path).cloned();
1653 (headers, None)
1654 };
1655
1656 // if html is Some(_), it means that `was_error_response` is true and we're not
1657 // actually going to cache this route, just return it as HTML
1658 //
1659 // this if for thing like 404s, where we do not want to cache an endless series of
1660 // typos (or malicious requests)
1661 let mut res = AxumResponse(match html {
1662 Some(html) => axum::response::Html(html).into_response(),
1663 None => match ServeFile::new(path).oneshot(req).await {
1664 Ok(res) => res.into_response(),
1665 Err(err) => (
1666 StatusCode::INTERNAL_SERVER_ERROR,
1667 format!("Something went wrong: {err}"),
1668 )
1669 .into_response(),
1670 },
1671 });
1672
1673 if let Some(options) = response_options {
1674 res.extend_response(&options);
1675 }
1676
1677 res.0
1678 })
1679 }
1680}
1681
1682/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
1683/// having to use wildcards or manually define all routes in multiple places.
1684pub trait LeptosRoutes<S>
1685where
1686 S: Clone + Send + Sync + 'static,
1687 LeptosOptions: FromRef<S>,
1688{
1689 /// Adds routes to the Axum router that have either
1690 /// 1) been generated by `leptos_router`, or
1691 /// 2) handle a server function.
1692 fn leptos_routes<IV>(
1693 self,
1694 options: &S,
1695 paths: Vec<AxumRouteListing>,
1696 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
1697 ) -> Self
1698 where
1699 IV: IntoView + 'static;
1700
1701 /// Adds routes to the Axum router that have either
1702 /// 1) been generated by `leptos_router`, or
1703 /// 2) handle a server function.
1704 ///
1705 /// Runs `additional_context` to provide additional data to the reactive system via context,
1706 /// when handling a route.
1707 fn leptos_routes_with_context<IV>(
1708 self,
1709 options: &S,
1710 paths: Vec<AxumRouteListing>,
1711 additional_context: impl Fn() + 'static + Clone + Send + Sync,
1712 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
1713 ) -> Self
1714 where
1715 IV: IntoView + 'static;
1716
1717 /// Extends the Axum router with the given paths, and handles the requests with the given
1718 /// handler.
1719 fn leptos_routes_with_handler<H, T>(
1720 self,
1721 paths: Vec<AxumRouteListing>,
1722 handler: H,
1723 ) -> Self
1724 where
1725 H: axum::handler::Handler<T, S>,
1726 T: 'static;
1727}
1728
1729trait AxumPath {
1730 fn to_axum_path(&self) -> String;
1731}
1732
1733impl AxumPath for Vec<PathSegment> {
1734 fn to_axum_path(&self) -> String {
1735 let mut path = String::new();
1736 for segment in self.iter() {
1737 // TODO trailing slash handling
1738 let raw = segment.as_raw_str();
1739 if !raw.is_empty() && !raw.starts_with('/') {
1740 path.push('/');
1741 }
1742 match segment {
1743 PathSegment::Static(s) => path.push_str(s),
1744 PathSegment::Param(s) => {
1745 path.push('{');
1746 path.push_str(s);
1747 path.push('}');
1748 }
1749 PathSegment::Splat(s) => {
1750 path.push('{');
1751 path.push('*');
1752 path.push_str(s);
1753 path.push('}');
1754 }
1755 PathSegment::Unit => {}
1756 PathSegment::OptionalParam(_) => {
1757 #[cfg(feature = "tracing")]
1758 tracing::error!(
1759 "to_axum_path should only be called on expanded \
1760 paths, which do not have OptionalParam any longer"
1761 );
1762 Default::default()
1763 }
1764 }
1765 }
1766 path
1767 }
1768}
1769
1770/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
1771/// to those paths to Leptos's renderer.
1772impl<S> LeptosRoutes<S> for axum::Router<S>
1773where
1774 S: Clone + Send + Sync + 'static,
1775 LeptosOptions: FromRef<S>,
1776{
1777 #[cfg_attr(
1778 feature = "tracing",
1779 tracing::instrument(level = "trace", fields(error), skip_all)
1780 )]
1781 fn leptos_routes<IV>(
1782 self,
1783 state: &S,
1784 paths: Vec<AxumRouteListing>,
1785 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
1786 ) -> Self
1787 where
1788 IV: IntoView + 'static,
1789 {
1790 self.leptos_routes_with_context(state, paths, || {}, app_fn)
1791 }
1792
1793 #[cfg_attr(
1794 feature = "tracing",
1795 tracing::instrument(level = "trace", fields(error), skip_all)
1796 )]
1797 fn leptos_routes_with_context<IV>(
1798 self,
1799 state: &S,
1800 paths: Vec<AxumRouteListing>,
1801 additional_context: impl Fn() + 'static + Clone + Send + Sync,
1802 app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
1803 ) -> Self
1804 where
1805 IV: IntoView + 'static,
1806 {
1807 init_executor();
1808
1809 // S represents the router's finished state allowing us to provide
1810 // it to the user's server functions.
1811 let state = state.clone();
1812 let cx_with_state = move || {
1813 provide_context::<S>(state.clone());
1814 additional_context();
1815 };
1816
1817 let mut router = self;
1818
1819 let excluded = paths
1820 .iter()
1821 .filter(|&p| p.exclude)
1822 .map(|p| p.path.as_str())
1823 .collect::<HashSet<_>>();
1824
1825 // register server functions
1826 for (path, method) in server_fn::axum::server_fn_paths() {
1827 let cx_with_state = cx_with_state.clone();
1828 let handler = move |req: Request<Body>| async move {
1829 handle_server_fns_with_context(cx_with_state, req).await
1830 };
1831
1832 if !excluded.contains(path) {
1833 router = router.route(
1834 path,
1835 match method {
1836 Method::GET => get(handler),
1837 Method::POST => post(handler),
1838 Method::PUT => put(handler),
1839 Method::DELETE => delete(handler),
1840 Method::PATCH => patch(handler),
1841 _ => {
1842 panic!(
1843 "Unsupported server function HTTP method: \
1844 {method:?}"
1845 );
1846 }
1847 },
1848 );
1849 }
1850 }
1851
1852 // register router paths
1853 for listing in paths.iter().filter(|p| !p.exclude) {
1854 let path = listing.path();
1855
1856 for method in listing.methods() {
1857 let cx_with_state = cx_with_state.clone();
1858 let cx_with_state_and_method = move || {
1859 provide_context(method);
1860 cx_with_state();
1861 };
1862 router = if matches!(listing.mode(), SsrMode::Static(_)) {
1863 #[cfg(feature = "default")]
1864 {
1865 router.route(
1866 path,
1867 get(handle_static_route(
1868 cx_with_state_and_method.clone(),
1869 app_fn.clone(),
1870 listing.regenerate.clone(),
1871 )),
1872 )
1873 }
1874 #[cfg(not(feature = "default"))]
1875 {
1876 panic!(
1877 "Static routes are not currently supported on \
1878 WASM32 server targets."
1879 );
1880 }
1881 } else {
1882 router.route(
1883 path,
1884 match listing.mode() {
1885 SsrMode::OutOfOrder => {
1886 let s = render_app_to_stream_with_context(
1887 cx_with_state_and_method.clone(),
1888 app_fn.clone(),
1889 );
1890 match method {
1891 leptos_router::Method::Get => get(s),
1892 leptos_router::Method::Post => post(s),
1893 leptos_router::Method::Put => put(s),
1894 leptos_router::Method::Delete => delete(s),
1895 leptos_router::Method::Patch => patch(s),
1896 }
1897 }
1898 SsrMode::PartiallyBlocked => {
1899 let s = render_app_to_stream_with_context_and_replace_blocks(
1900 cx_with_state_and_method.clone(),
1901 app_fn.clone(),
1902 true
1903 );
1904 match method {
1905 leptos_router::Method::Get => get(s),
1906 leptos_router::Method::Post => post(s),
1907 leptos_router::Method::Put => put(s),
1908 leptos_router::Method::Delete => delete(s),
1909 leptos_router::Method::Patch => patch(s),
1910 }
1911 }
1912 SsrMode::InOrder => {
1913 let s = render_app_to_stream_in_order_with_context(
1914 cx_with_state_and_method.clone(),
1915 app_fn.clone(),
1916 );
1917 match method {
1918 leptos_router::Method::Get => get(s),
1919 leptos_router::Method::Post => post(s),
1920 leptos_router::Method::Put => put(s),
1921 leptos_router::Method::Delete => delete(s),
1922 leptos_router::Method::Patch => patch(s),
1923 }
1924 }
1925 SsrMode::Async => {
1926 let s = render_app_async_with_context(
1927 cx_with_state_and_method.clone(),
1928 app_fn.clone(),
1929 );
1930 match method {
1931 leptos_router::Method::Get => get(s),
1932 leptos_router::Method::Post => post(s),
1933 leptos_router::Method::Put => put(s),
1934 leptos_router::Method::Delete => delete(s),
1935 leptos_router::Method::Patch => patch(s),
1936 }
1937 }
1938 _ => unreachable!()
1939 },
1940 )
1941 };
1942 }
1943 }
1944
1945 router
1946 }
1947
1948 #[cfg_attr(
1949 feature = "tracing",
1950 tracing::instrument(level = "trace", fields(error), skip_all)
1951 )]
1952 fn leptos_routes_with_handler<H, T>(
1953 self,
1954 paths: Vec<AxumRouteListing>,
1955 handler: H,
1956 ) -> Self
1957 where
1958 H: axum::handler::Handler<T, S>,
1959 T: 'static,
1960 {
1961 let mut router = self;
1962 for listing in paths.iter().filter(|p| !p.exclude) {
1963 for method in listing.methods() {
1964 router = router.route(
1965 listing.path(),
1966 match method {
1967 leptos_router::Method::Get => get(handler.clone()),
1968 leptos_router::Method::Post => post(handler.clone()),
1969 leptos_router::Method::Put => put(handler.clone()),
1970 leptos_router::Method::Delete => {
1971 delete(handler.clone())
1972 }
1973 leptos_router::Method::Patch => patch(handler.clone()),
1974 },
1975 );
1976 }
1977 }
1978 router
1979 }
1980}
1981
1982/// A helper to make it easier to use Axum extractors in server functions.
1983///
1984/// It is generic over some type `T` that implements [`FromRequestParts`] and can
1985/// therefore be used in an extractor. The compiler can often infer this type.
1986///
1987/// Any error that occurs during extraction is converted to a [`ServerFnError`].
1988///
1989/// ```rust
1990/// use leptos::prelude::*;
1991///
1992/// #[server]
1993/// pub async fn request_method() -> Result<String, ServerFnError> {
1994/// use axum::http::Method;
1995/// use leptos_axum::extract;
1996///
1997/// // you can extract anything that a regular Axum extractor can extract
1998/// // from the head (not from the body of the request)
1999/// let method: Method = extract().await?;
2000///
2001/// Ok(format!("{method:?}"))
2002/// }
2003/// ```
2004pub async fn extract<T>() -> Result<T, ServerFnErrorErr>
2005where
2006 T: Sized + FromRequestParts<()>,
2007 T::Rejection: Debug,
2008{
2009 extract_with_state::<T, ()>(&()).await
2010}
2011
2012/// A helper to make it easier to use Axum extractors in server functions. This
2013/// function is compatible with extractors that require access to `State`.
2014///
2015/// It is generic over some type `T` that implements [`FromRequestParts`] and can
2016/// therefore be used in an extractor. The compiler can often infer this type.
2017///
2018/// Any error that occurs during extraction is converted to a [`ServerFnError`].
2019pub async fn extract_with_state<T, S>(state: &S) -> Result<T, ServerFnErrorErr>
2020where
2021 T: Sized + FromRequestParts<S>,
2022 T::Rejection: Debug,
2023{
2024 let mut parts = use_context::<Parts>().ok_or_else(|| {
2025 ServerFnErrorErr::ServerError(
2026 "should have had Parts provided by the leptos_axum integration"
2027 .to_string(),
2028 )
2029 })?;
2030 T::from_request_parts(&mut parts, state)
2031 .await
2032 .map_err(|e| ServerFnErrorErr::ServerError(format!("{e:?}")))
2033}
2034
2035/// A reasonable handler for serving static files (like JS/WASM/CSS) and 404 errors.
2036///
2037/// This is provided as a convenience, but is a fairly simple function. If you need to adapt it,
2038/// simply reuse the source code of this function in your own application. A more compositional
2039/// implementation is offered by [`ErrorHandler`] as it implements a tower [`Service`] which
2040/// may be composed with other tower services.
2041///
2042/// [`Service`]: tower::Service
2043#[cfg(feature = "default")]
2044pub fn file_and_error_handler_with_context<S, IV>(
2045 additional_context: impl Fn() + 'static + Clone + Send,
2046 shell: impl Fn(LeptosOptions) -> IV + 'static + Clone + Send,
2047) -> impl Fn(
2048 Uri,
2049 State<S>,
2050 Request<Body>,
2051) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
2052 + Clone
2053 + Send
2054 + 'static
2055where
2056 IV: IntoView + 'static,
2057 S: Send + Sync + Clone + 'static,
2058 LeptosOptions: FromRef<S>,
2059{
2060 move |uri: Uri, State(state): State<S>, req: Request<Body>| {
2061 Box::pin({
2062 let additional_context = additional_context.clone();
2063 let shell = shell.clone();
2064 async move {
2065 let options = LeptosOptions::from_ref(&state);
2066 let res =
2067 get_static_file(uri, &options.site_root, req.headers());
2068 let res = res.await.unwrap();
2069
2070 if res.status() == StatusCode::OK {
2071 let owner = Owner::new();
2072 owner.with(|| {
2073 additional_context();
2074 let res = res.into_response();
2075 if let Some(response_options) =
2076 use_context::<ResponseOptions>()
2077 {
2078 let mut res = AxumResponse(res);
2079 res.extend_response(&response_options);
2080 res.0
2081 } else {
2082 res
2083 }
2084 })
2085 } else {
2086 let mut res = handle_response_inner(
2087 move || {
2088 provide_context(state.clone());
2089 additional_context();
2090 },
2091 move || shell(options),
2092 req,
2093 |app, chunks, _supports_ooo| {
2094 Box::pin(async move {
2095 let app = if cfg!(feature = "islands-router") {
2096 app.to_html_stream_in_order_branching()
2097 } else {
2098 app.to_html_stream_in_order()
2099 };
2100 let app = app.collect::<String>().await;
2101 let chunks = chunks();
2102 Box::pin(once(async move { app }).chain(chunks))
2103 as PinnedStream<String>
2104 })
2105 },
2106 )
2107 .await;
2108
2109 // set the status to 404
2110 // but if the status was already set (for example, to a 302 redirect) don't
2111 // overwrite it
2112 let status = res.status_mut();
2113 if *status == StatusCode::OK {
2114 *res.status_mut() = StatusCode::NOT_FOUND;
2115 }
2116
2117 res
2118 }
2119 }
2120 })
2121 }
2122}
2123
2124/// A reasonable handler for serving static files (like JS/WASM/CSS) and 404 errors.
2125///
2126/// This is provided as a convenience, but is a fairly simple function. If you need to adapt it,
2127/// simply reuse the source code of this function in your own application. A more compositional
2128/// implementation is offered by [`ErrorHandler`] as it implements a tower [`Service`] which
2129/// may be composed with other tower services.
2130///
2131/// [`Service`]: tower::Service
2132#[cfg(feature = "default")]
2133pub fn file_and_error_handler<S, IV>(
2134 shell: impl Fn(LeptosOptions) -> IV + 'static + Clone + Send,
2135) -> impl Fn(
2136 Uri,
2137 State<S>,
2138 Request<Body>,
2139) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
2140 + Clone
2141 + Send
2142 + 'static
2143where
2144 IV: IntoView + 'static,
2145 S: Send + Sync + Clone + 'static,
2146 LeptosOptions: FromRef<S>,
2147{
2148 file_and_error_handler_with_context(move || (), shell)
2149}
2150
2151#[cfg(feature = "default")]
2152async fn get_static_file(
2153 uri: Uri,
2154 root: &str,
2155 headers: &HeaderMap<HeaderValue>,
2156) -> Result<Response<Body>, (StatusCode, String)> {
2157 use axum::http::header::ACCEPT_ENCODING;
2158
2159 let req = Request::builder().uri(uri);
2160
2161 let req = match headers.get(ACCEPT_ENCODING) {
2162 Some(value) => req.header(ACCEPT_ENCODING, value),
2163 None => req,
2164 };
2165
2166 let req = req.body(Body::empty()).unwrap();
2167 // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
2168 // This path is relative to the cargo root
2169 match ServeDir::new(root)
2170 .precompressed_gzip()
2171 .precompressed_br()
2172 .oneshot(req)
2173 .await
2174 {
2175 Ok(res) => Ok(res.into_response()),
2176 Err(err) => Err((
2177 StatusCode::INTERNAL_SERVER_ERROR,
2178 format!("Something went wrong: {err}"),
2179 )),
2180 }
2181}
2182
2183/// A helper to create a [`ServeDir`] service for the static files under
2184/// `LEPTOS_SITE_ROOT`. This may be further configured before being assigned
2185/// as the fallback service, or be attached as a service route on the router,
2186/// typically with the path derived from [`site_pkg_dir_service_route_path`].
2187///
2188/// [`ServeDir`]: tower_http::services::ServeDir
2189#[cfg(feature = "default")]
2190pub fn site_pkg_dir_service(options: &LeptosOptions) -> ServeDir {
2191 ServeDir::new(&*options.site_root)
2192 .precompressed_gzip()
2193 .precompressed_br()
2194}
2195
2196/// A helper for constructing the axum route path from the `LeptosOptions`, can be used
2197/// in conjunction with the [`ServeDir`] service produced by [`site_pkg_dir_service`]
2198/// for setting up a routed site pkg service with [`Router::route_service`].
2199///
2200/// [`ServeDir`]: tower_http::services::ServeDir
2201pub fn site_pkg_dir_service_route_path(options: &LeptosOptions) -> String {
2202 // The path of the route being built will be constained to serve only the
2203 // contents of `site_pkg_dir` to avoid conflicts with the root routes.
2204 let mut path = String::new();
2205 // While it shouldn't start with a '/', but check anyway.
2206 if !options.site_pkg_dir.starts_with('/') {
2207 path.push('/');
2208 }
2209 path.push_str(&options.site_pkg_dir);
2210 if !path.ends_with('/') {
2211 path.push('/');
2212 }
2213 path.push_str("{*path}");
2214 path
2215}