Skip to main content

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}