Skip to main content

leptos_actix/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3
4//! Provides functions to easily integrate Leptos with Actix.
5//!
6//! For more details on how to use the integrations, see the
7//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
8//! directory in the Leptos repository.
9
10use actix_files::NamedFile;
11use actix_http::header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER};
12use actix_web::{
13    dev::{ServiceFactory, ServiceRequest},
14    http::header,
15    test,
16    web::{Data, Payload, ServiceConfig},
17    *,
18};
19use futures::{stream::once, Stream, StreamExt};
20use http::StatusCode;
21use hydration_context::SsrSharedContext;
22use leptos::{
23    config::LeptosOptions,
24    context::{provide_context, use_context},
25    hydration::IslandsRouterNavigation,
26    prelude::expect_context,
27    reactive::{computed::ScopedFuture, owner::Owner},
28    IntoView,
29};
30use leptos_integration_utils::{
31    BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
32};
33use leptos_meta::ServerMetaContext;
34use leptos_router::{
35    components::provide_server_redirect,
36    location::RequestUrl,
37    static_routes::{RegenerationFn, ResolvedStaticPath},
38    ExpandOptionals, Method, PathSegment, RouteList, RouteListing, SsrMode,
39};
40use or_poisoned::OrPoisoned;
41use send_wrapper::SendWrapper;
42use server_fn::{
43    error::ServerFnErrorErr, redirect::REDIRECT_HEADER,
44    request::actix::ActixRequest,
45};
46use std::{
47    collections::{HashMap, HashSet},
48    fmt::{Debug, Display},
49    future::Future,
50    ops::{Deref, DerefMut},
51    path::Path,
52    sync::{Arc, LazyLock, RwLock},
53};
54
55/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
56/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
57#[derive(Debug, Clone, Default)]
58pub struct ResponseParts {
59    /// If provided, this will overwrite any other status code for this response.
60    pub status: Option<StatusCode>,
61    /// The map of headers that should be added to the response.
62    pub headers: header::HeaderMap,
63}
64
65impl ResponseParts {
66    /// Insert a header, overwriting any previous value with the same key
67    pub fn insert_header(
68        &mut self,
69        key: header::HeaderName,
70        value: header::HeaderValue,
71    ) {
72        self.headers.insert(key, value);
73    }
74
75    /// Append a header, leaving any header with the same key intact
76    pub fn append_header(
77        &mut self,
78        key: header::HeaderName,
79        value: header::HeaderValue,
80    ) {
81        self.headers.append(key, value);
82    }
83}
84
85/// A wrapper for an Actix [`HttpRequest`] that allows it to be used in an
86/// `Send`/`Sync` setting like Leptos's Context API.
87#[derive(Debug, Clone)]
88pub struct Request(SendWrapper<HttpRequest>);
89
90impl Request {
91    /// Wraps an existing Actix request.
92    pub fn new(req: &HttpRequest) -> Self {
93        Self(SendWrapper::new(req.clone()))
94    }
95
96    /// Consumes the wrapper and returns the inner Actix request.
97    pub fn into_inner(self) -> HttpRequest {
98        self.0.take()
99    }
100}
101
102impl Deref for Request {
103    type Target = HttpRequest;
104
105    fn deref(&self) -> &Self::Target {
106        &self.0
107    }
108}
109
110impl DerefMut for Request {
111    fn deref_mut(&mut self) -> &mut Self::Target {
112        &mut self.0
113    }
114}
115
116/// Allows you to override details of the HTTP response like the status code and add Headers/Cookies.
117#[derive(Debug, Clone, Default)]
118pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
119
120impl ResponseOptions {
121    /// A simpler way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`.
122    pub fn overwrite(&self, parts: ResponseParts) {
123        let mut writable = self.0.write().or_poisoned();
124        *writable = parts
125    }
126    /// Set the status of the returned Response.
127    pub fn set_status(&self, status: StatusCode) {
128        let mut writeable = self.0.write().or_poisoned();
129        let res_parts = &mut *writeable;
130        res_parts.status = Some(status);
131    }
132    /// Insert a header, overwriting any previous value with the same key.
133    pub fn insert_header(
134        &self,
135        key: header::HeaderName,
136        value: header::HeaderValue,
137    ) {
138        let mut writeable = self.0.write().or_poisoned();
139        let res_parts = &mut *writeable;
140        res_parts.headers.insert(key, value);
141    }
142    /// Append a header, leaving any header with the same key intact.
143    pub fn append_header(
144        &self,
145        key: header::HeaderName,
146        value: header::HeaderValue,
147    ) {
148        let mut writeable = self.0.write().or_poisoned();
149        let res_parts = &mut *writeable;
150        res_parts.headers.append(key, value);
151    }
152}
153
154struct ActixResponse(HttpResponse);
155
156impl ExtendResponse for ActixResponse {
157    type ResponseOptions = ResponseOptions;
158
159    fn from_stream(
160        stream: impl Stream<Item = String> + Send + 'static,
161    ) -> Self {
162        ActixResponse(
163            HttpResponse::Ok()
164                .content_type("text/html")
165                .streaming(stream.map(|chunk| {
166                    Ok(web::Bytes::from(chunk)) as Result<web::Bytes>
167                })),
168        )
169    }
170
171    fn extend_response(&mut self, res_options: &Self::ResponseOptions) {
172        let mut res_options = res_options.0.write().or_poisoned();
173
174        let headers = self.0.headers_mut();
175        for (key, value) in std::mem::take(&mut res_options.headers) {
176            headers.append(key, value);
177        }
178
179        // Set status to what is returned in the function
180        if let Some(status) = res_options.status {
181            *self.0.status_mut() = status;
182        }
183    }
184
185    fn set_default_content_type(&mut self, content_type: &str) {
186        let headers = self.0.headers_mut();
187        if !headers.contains_key(header::CONTENT_TYPE) {
188            // Set the Content Type headers on all responses. This makes Firefox show the page source
189            // without complaining
190            headers.insert(
191                header::CONTENT_TYPE,
192                HeaderValue::from_str(content_type).unwrap(),
193            );
194        }
195    }
196}
197
198/// Provides an easy way to redirect the user from within a server function.
199///
200/// Calling `redirect` in a server function will redirect the browser in three
201/// situations:
202/// 1. A server function that is calling in a [blocking
203///    resource](leptos::server::Resource::new_blocking).
204/// 2. A server function that is called from WASM running in the client (e.g., a dispatched action
205///    or a spawned `Future`).
206/// 3. A `<form>` submitted to the server function endpoint using default browser APIs (often due
207///    to using [`ActionForm`](leptos::form::ActionForm) without JS/WASM present.)
208///
209/// Using it with a non-blocking [`Resource`](leptos::server::Resource) will not work if you are using streaming rendering,
210/// as the response's headers will already have been sent by the time the server function calls `redirect()`.
211///
212/// ### Implementation
213///
214/// This sets the `Location` header to the URL given.
215///
216/// If the route or server function in which this is called is being accessed
217/// by an ordinary `GET` request or an HTML `<form>` without any enhancement, it also sets a
218/// status code of `302` for a temporary redirect. (This is determined by whether the `Accept`
219/// header contains `text/html` as it does for an ordinary navigation.)
220///
221/// Otherwise, it sets a custom header that indicates to the client that it should redirect,
222/// without actually setting the status code. This means that the client will not follow the
223/// redirect, and can therefore return the value of the server function and then handle
224/// the redirect with client-side routing.
225#[cfg_attr(
226    feature = "tracing",
227    tracing::instrument(level = "trace", fields(error), skip_all)
228)]
229pub fn redirect(path: &str) {
230    if let (Some(req), Some(res)) =
231        (use_context::<Request>(), use_context::<ResponseOptions>())
232    {
233        // insert the Location header in any case
234        res.insert_header(
235            header::LOCATION,
236            header::HeaderValue::from_str(path)
237                .expect("Failed to create HeaderValue"),
238        );
239
240        let accepts_html = req
241            .headers()
242            .get(ACCEPT)
243            .and_then(|v| v.to_str().ok())
244            .map(|v| v.contains("text/html"))
245            .unwrap_or(false);
246        if accepts_html {
247            // if the request accepts text/html, it's a plain form request and needs
248            // to have the 302 code set
249            res.set_status(StatusCode::FOUND);
250        } else {
251            // otherwise, we sent it from the server fn client and actually don't want
252            // to set a real redirect, as this will break the ability to return data
253            // instead, set the REDIRECT_HEADER to indicate that the client should redirect
254            res.insert_header(
255                HeaderName::from_static(REDIRECT_HEADER),
256                HeaderValue::from_str("").unwrap(),
257            );
258        }
259    } else {
260        let msg = "Couldn't retrieve either Parts or ResponseOptions while \
261                   trying to redirect().";
262
263        #[cfg(feature = "tracing")]
264        tracing::warn!("{}", &msg);
265
266        #[cfg(not(feature = "tracing"))]
267        eprintln!("{}", &msg);
268    }
269}
270
271/// An Actix [struct@Route](actix_web::Route) that listens for a `POST` request with
272/// Leptos server function arguments in the body, runs the server function if found,
273/// and returns the resulting [HttpResponse].
274///
275/// This can then be set up at an appropriate route in your application:
276///
277/// ```no_run
278/// use actix_web::*;
279///
280/// fn register_server_functions() {
281///   // call ServerFn::register() for each of the server functions you've defined
282/// }
283///
284/// # #[cfg(feature = "default")]
285/// #[actix_web::main]
286/// async fn main() -> std::io::Result<()> {
287///     // make sure you actually register your server functions
288///     register_server_functions();
289///
290///     HttpServer::new(|| {
291///         App::new()
292///             // "/api" should match the prefix, if any, declared when defining server functions
293///             // {tail:.*} passes the remainder of the URL as the server function name
294///             .route("/api/{tail:.*}", leptos_actix::handle_server_fns())
295///     })
296///     .bind(("127.0.0.1", 8080))?
297///     .run()
298///     .await
299/// }
300/// # #[cfg(not(feature = "default"))]
301/// # fn main() {}
302/// ```
303///
304/// ## Provided Context Types
305/// This function always provides context values including the following types:
306/// - [ResponseOptions]
307/// - [Request]
308#[cfg_attr(
309    feature = "tracing",
310    tracing::instrument(level = "trace", fields(error), skip_all)
311)]
312pub fn handle_server_fns() -> Route {
313    handle_server_fns_with_context(|| {})
314}
315
316/// An Actix [struct@Route](actix_web::Route) that listens for `GET` or `POST` requests with
317/// Leptos server function arguments in the URL (`GET`) or body (`POST`),
318/// runs the server function if found, and returns the resulting [HttpResponse].
319///
320/// This can then be set up at an appropriate route in your application:
321///
322/// This version allows you to pass in a closure that adds additional route data to the
323/// context, allowing you to pass in info about the route or user from Actix, or other info.
324///
325/// **NOTE**: If your server functions expect a context, make sure to provide it both in
326/// [`handle_server_fns_with_context`] **and** in [`LeptosRoutes::leptos_routes_with_context`] (or whatever
327/// rendering method you are using). During SSR, server functions are called by the rendering
328/// method, while subsequent calls from the client are handled by the server function handler.
329/// The same context needs to be provided to both handlers.
330///
331/// ## Provided Context Types
332/// This function always provides context values including the following types:
333/// - [ResponseOptions]
334/// - [Request]
335#[cfg_attr(
336    feature = "tracing",
337    tracing::instrument(level = "trace", fields(error), skip_all)
338)]
339pub fn handle_server_fns_with_context(
340    additional_context: impl Fn() + 'static + Clone + Send,
341) -> Route {
342    web::to(move |req: HttpRequest, payload: Payload| {
343        let additional_context = additional_context.clone();
344        async move {
345            let additional_context = additional_context.clone();
346
347            let path = req.path();
348            let method = req.method();
349            if let Some(mut service) =
350                server_fn::actix::get_server_fn_service(path, method)
351            {
352                let owner = Owner::new();
353                owner
354                    .with(|| {
355                        ScopedFuture::new(async move {
356                            provide_context(Request::new(&req));
357                            let res_options = ResponseOptions::default();
358                            provide_context(res_options.clone());
359                            additional_context();
360
361                            // store Accepts and Referer in case we need them for redirect (below)
362                            let accepts_html = req
363                                .headers()
364                                .get(ACCEPT)
365                                .and_then(|v| v.to_str().ok())
366                                .map(|v| v.contains("text/html"))
367                                .unwrap_or(false);
368                            let referrer = req.headers().get(REFERER).cloned();
369
370                            // actually run the server fn
371                            let mut res = ActixResponse(
372                                service
373                                    .run(ActixRequest::from((req, payload)))
374                                    .await
375                                    .take(),
376                            );
377
378                            // if it accepts text/html (i.e., is a plain form post) and doesn't already have a
379                            // Location set, then redirect to the Referer
380                            if accepts_html {
381                                if let Some(referrer) = referrer {
382                                    let has_location =
383                                        res.0.headers().get(LOCATION).is_some();
384                                    if !has_location {
385                                        *res.0.status_mut() = StatusCode::FOUND;
386                                        res.0
387                                            .headers_mut()
388                                            .insert(LOCATION, referrer);
389                                    }
390                                }
391                            }
392
393                            // the Location header may have been set to Referer, so any redirection by the
394                            // user must overwrite it
395                            {
396                                let mut res_options =
397                                    res_options.0.write().or_poisoned();
398                                let headers = res.0.headers_mut();
399
400                                for location in
401                                    res_options.headers.remove(header::LOCATION)
402                                {
403                                    headers.insert(header::LOCATION, location);
404                                }
405                            }
406
407                            // apply status code and headers if user changed them
408                            res.extend_response(&res_options);
409                            res.0
410                        })
411                    })
412                    .await
413            } else {
414                HttpResponse::BadRequest().body(format!(
415                    "Could not find a server function at the route {:?}. \
416                     \n\nIt's likely that either
417                         1. The API prefix you specify in the `#[server]` \
418                     macro doesn't match the prefix at which your server \
419                     function handler is mounted, or \n2. You are on a \
420                     platform that doesn't support automatic server function \
421                     registration and you need to call \
422                     ServerFn::register_explicit() on the server function \
423                     type, somewhere in your `main` function.",
424                    req.path()
425                ))
426            }
427        }
428    })
429}
430
431/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
432/// to route it using [leptos_router], serving an HTML stream of your application. The stream
433/// will include fallback content for any `<Suspense/>` nodes, and be immediately interactive,
434/// but requires some client-side JavaScript.
435///
436/// This can then be set up at an appropriate route in your application:
437/// ```no_run
438/// use actix_web::{App, HttpServer};
439/// use leptos::prelude::*;
440/// use leptos_router::Method;
441/// use std::{env, net::SocketAddr};
442///
443/// #[component]
444/// fn MyApp() -> impl IntoView {
445///     view! { <main>"Hello, world!"</main> }
446/// }
447///
448/// # #[cfg(feature = "default")]
449/// #[actix_web::main]
450/// async fn main() -> std::io::Result<()> {
451///     let conf = get_configuration(Some("Cargo.toml")).unwrap();
452///     let addr = conf.leptos_options.site_addr.clone();
453///     HttpServer::new(move || {
454///         let leptos_options = &conf.leptos_options;
455///
456///         App::new()
457///             // {tail:.*} passes the remainder of the URL as the route
458///             // the actual routing will be handled by `leptos_router`
459///             .route(
460///                 "/{tail:.*}",
461///                 leptos_actix::render_app_to_stream(MyApp, Method::Get),
462///             )
463///     })
464///     .bind(&addr)?
465///     .run()
466///     .await
467/// }
468/// # #[cfg(not(feature = "default"))]
469/// # fn main() {}
470/// ```
471///
472/// ## Provided Context Types
473/// This function always provides context values including the following types:
474/// - [ResponseOptions]
475/// - [Request]
476/// - [MetaContext](leptos_meta::MetaContext)
477#[cfg_attr(
478    feature = "tracing",
479    tracing::instrument(level = "trace", fields(error), skip_all)
480)]
481pub fn render_app_to_stream<IV>(
482    app_fn: impl Fn() -> IV + Clone + Send + 'static,
483    method: Method,
484) -> Route
485where
486    IV: IntoView + 'static,
487{
488    render_app_to_stream_with_context(|| {}, app_fn, method)
489}
490
491/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
492/// to route it using [leptos_router], serving an in-order HTML stream of your application.
493/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
494/// sending down its HTML. The app will become interactive once it has fully loaded.
495///
496/// This can then be set up at an appropriate route in your application:
497/// ```no_run
498/// use actix_web::{App, HttpServer};
499/// use leptos::prelude::*;
500/// use leptos_router::Method;
501/// use std::{env, net::SocketAddr};
502///
503/// #[component]
504/// fn MyApp() -> impl IntoView {
505///     view! { <main>"Hello, world!"</main> }
506/// }
507///
508/// # #[cfg(feature = "default")]
509/// #[actix_web::main]
510/// async fn main() -> std::io::Result<()> {
511///     let conf = get_configuration(Some("Cargo.toml")).unwrap();
512///     let addr = conf.leptos_options.site_addr.clone();
513///     HttpServer::new(move || {
514///         let leptos_options = &conf.leptos_options;
515///
516///         App::new()
517///             // {tail:.*} passes the remainder of the URL as the route
518///             // the actual routing will be handled by `leptos_router`
519///             .route(
520///                 "/{tail:.*}",
521///                 leptos_actix::render_app_to_stream_in_order(
522///                     MyApp,
523///                     Method::Get,
524///                 ),
525///             )
526///     })
527///     .bind(&addr)?
528///     .run()
529///     .await
530/// }
531///
532/// # #[cfg(not(feature = "default"))]
533/// # fn main() {}
534/// ```
535///
536/// ## Provided Context Types
537/// This function always provides context values including the following types:
538/// - [ResponseOptions]
539/// - [Request]
540#[cfg_attr(
541    feature = "tracing",
542    tracing::instrument(level = "trace", fields(error), skip_all)
543)]
544pub fn render_app_to_stream_in_order<IV>(
545    app_fn: impl Fn() -> IV + Clone + Send + 'static,
546    method: Method,
547) -> Route
548where
549    IV: IntoView + 'static,
550{
551    render_app_to_stream_in_order_with_context(|| {}, app_fn, method)
552}
553
554/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
555/// to route it using [leptos_router], asynchronously rendering an HTML page after all
556/// `async` resources have loaded.
557///
558/// This can then be set up at an appropriate route in your application:
559/// ```no_run
560/// use actix_web::{App, HttpServer};
561/// use leptos::prelude::*;
562/// use leptos_router::Method;
563/// use std::{env, net::SocketAddr};
564///
565/// #[component]
566/// fn MyApp() -> impl IntoView {
567///     view! { <main>"Hello, world!"</main> }
568/// }
569///
570/// # #[cfg(feature = "default")]
571/// #[actix_web::main]
572/// async fn main() -> std::io::Result<()> {
573///     let conf = get_configuration(Some("Cargo.toml")).unwrap();
574///     let addr = conf.leptos_options.site_addr.clone();
575///     HttpServer::new(move || {
576///         let leptos_options = &conf.leptos_options;
577///
578///         App::new()
579///             // {tail:.*} passes the remainder of the URL as the route
580///             // the actual routing will be handled by `leptos_router`
581///             .route(
582///                 "/{tail:.*}",
583///                 leptos_actix::render_app_async(MyApp, Method::Get),
584///             )
585///     })
586///     .bind(&addr)?
587///     .run()
588///     .await
589/// }
590/// # #[cfg(not(feature = "default"))]
591/// # fn main() {}
592/// ```
593///
594/// ## Provided Context Types
595/// This function always provides context values including the following types:
596/// - [ResponseOptions]
597/// - [Request]
598#[cfg_attr(
599    feature = "tracing",
600    tracing::instrument(level = "trace", fields(error), skip_all)
601)]
602pub fn render_app_async<IV>(
603    app_fn: impl Fn() -> IV + Clone + Send + 'static,
604    method: Method,
605) -> Route
606where
607    IV: IntoView + 'static,
608{
609    render_app_async_with_context(|| {}, app_fn, method)
610}
611
612/// Returns an Actix [struct@Route] that listens for a `GET` request and tries
613/// to route it using [leptos_router], serving an HTML stream of your application.
614///
615/// This function allows you to provide additional information to Leptos for your route.
616/// It could be used to pass in Path Info, Connection Info, or anything your heart desires.
617///
618/// ## Provided Context Types
619/// This function always provides context values including the following types:
620/// - [ResponseOptions]
621/// - [Request]
622#[cfg_attr(
623    feature = "tracing",
624    tracing::instrument(level = "trace", fields(error), skip_all)
625)]
626pub fn render_app_to_stream_with_context<IV>(
627    additional_context: impl Fn() + 'static + Clone + Send,
628    app_fn: impl Fn() -> IV + Clone + Send + 'static,
629    method: Method,
630) -> Route
631where
632    IV: IntoView + 'static,
633{
634    render_app_to_stream_with_context_and_replace_blocks(
635        additional_context,
636        app_fn,
637        method,
638        false,
639    )
640}
641
642/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
643/// to route it using [leptos_router], serving an HTML stream of your application.
644///
645/// This function allows you to provide additional information to Leptos for your route.
646/// It could be used to pass in Path Info, Connection Info, or anything your heart desires.
647///
648/// `replace_blocks` additionally lets you specify whether `<Suspense/>` fragments that read
649/// from blocking resources should be retrojected into the HTML that's initially served, rather
650/// than dynamically inserting them with JavaScript on the client. This means you will have
651/// better support if JavaScript is not enabled, in exchange for a marginally slower response time.
652///
653/// ## Provided Context Types
654/// This function always provides context values including the following types:
655/// - [ResponseOptions]
656/// - [Request]
657#[cfg_attr(
658    feature = "tracing",
659    tracing::instrument(level = "trace", fields(error), skip_all)
660)]
661pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
662    additional_context: impl Fn() + 'static + Clone + Send,
663    app_fn: impl Fn() -> IV + Clone + Send + 'static,
664    method: Method,
665    replace_blocks: bool,
666) -> Route
667where
668    IV: IntoView + 'static,
669{
670    _ = replace_blocks; // TODO
671    handle_response(
672        method,
673        additional_context,
674        app_fn,
675        |app, chunks, supports_ooo| {
676            Box::pin(async move {
677                let app = if cfg!(feature = "islands-router") {
678                    if supports_ooo {
679                        app.to_html_stream_out_of_order_branching()
680                    } else {
681                        app.to_html_stream_in_order_branching()
682                    }
683                } else if supports_ooo {
684                    app.to_html_stream_out_of_order()
685                } else {
686                    app.to_html_stream_in_order()
687                };
688                Box::pin(app.chain(chunks())) as PinnedStream<String>
689            })
690        },
691    )
692}
693
694/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
695/// to route it using [leptos_router], serving an in-order HTML stream of your application.
696///
697/// This function allows you to provide additional information to Leptos for your route.
698/// It could be used to pass in Path Info, Connection Info, or anything your heart desires.
699///
700/// ## Provided Context Types
701/// This function always provides context values including the following types:
702/// - [ResponseOptions]
703/// - [Request]
704/// - [MetaContext](leptos_meta::MetaContext)
705#[cfg_attr(
706    feature = "tracing",
707    tracing::instrument(level = "trace", fields(error), skip_all)
708)]
709pub fn render_app_to_stream_in_order_with_context<IV>(
710    additional_context: impl Fn() + 'static + Clone + Send,
711    app_fn: impl Fn() -> IV + Clone + Send + 'static,
712    method: Method,
713) -> Route
714where
715    IV: IntoView + 'static,
716{
717    handle_response(
718        method,
719        additional_context,
720        app_fn,
721        |app, chunks, _supports_ooo| {
722            Box::pin(async move {
723                let app = if cfg!(feature = "islands-router") {
724                    app.to_html_stream_in_order_branching()
725                } else {
726                    app.to_html_stream_in_order()
727                };
728                Box::pin(app.chain(chunks())) as PinnedStream<String>
729            })
730        },
731    )
732}
733
734/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
735/// to route it using [leptos_router], asynchronously serving the page once all `async`
736/// resources have loaded.
737///
738/// This function allows you to provide additional information to Leptos for your route.
739/// It could be used to pass in Path Info, Connection Info, or anything your heart desires.
740///
741/// ## Provided Context Types
742/// This function always provides context values including the following types:
743/// - [ResponseOptions]
744/// - [Request]
745#[cfg_attr(
746    feature = "tracing",
747    tracing::instrument(level = "trace", fields(error), skip_all)
748)]
749pub fn render_app_async_with_context<IV>(
750    additional_context: impl Fn() + 'static + Clone + Send,
751    app_fn: impl Fn() -> IV + Clone + Send + 'static,
752    method: Method,
753) -> Route
754where
755    IV: IntoView + 'static,
756{
757    handle_response(method, additional_context, app_fn, async_stream_builder)
758}
759
760fn async_stream_builder<IV>(
761    app: IV,
762    chunks: BoxedFnOnce<PinnedStream<String>>,
763    _supports_ooo: bool,
764) -> PinnedFuture<PinnedStream<String>>
765where
766    IV: IntoView + 'static,
767{
768    Box::pin(async move {
769        let app = if cfg!(feature = "islands-router") {
770            app.to_html_stream_in_order_branching()
771        } else {
772            app.to_html_stream_in_order()
773        };
774        let app = app.collect::<String>().await;
775        let chunks = chunks();
776        Box::pin(once(async move { app }).chain(chunks)) as PinnedStream<String>
777    })
778}
779
780#[cfg_attr(
781    feature = "tracing",
782    tracing::instrument(level = "trace", fields(error), skip_all)
783)]
784fn provide_contexts(
785    req: Request,
786    meta_context: &ServerMetaContext,
787    res_options: &ResponseOptions,
788) {
789    let path = leptos_corrected_path(&req);
790
791    provide_context(RequestUrl::new(&path));
792    provide_context(meta_context.clone());
793    provide_context(res_options.clone());
794    provide_context(req);
795    provide_server_redirect(redirect);
796    leptos::nonce::provide_nonce();
797}
798
799fn leptos_corrected_path(req: &HttpRequest) -> String {
800    let path = req.path();
801    let query = req.query_string();
802    if query.is_empty() {
803        "http://leptos".to_string() + path
804    } else {
805        "http://leptos".to_string() + path + "?" + query
806    }
807}
808
809#[allow(clippy::type_complexity)]
810fn handle_response<IV>(
811    method: Method,
812    additional_context: impl Fn() + 'static + Clone + Send,
813    app_fn: impl Fn() -> IV + Clone + Send + 'static,
814    stream_builder: fn(
815        IV,
816        BoxedFnOnce<PinnedStream<String>>,
817        bool,
818    ) -> PinnedFuture<PinnedStream<String>>,
819) -> Route
820where
821    IV: IntoView + 'static,
822{
823    let handler = move |req: HttpRequest| {
824        let app_fn = app_fn.clone();
825        let add_context = additional_context.clone();
826
827        async move {
828            let is_island_router_navigation = cfg!(feature = "islands-router")
829                && req.headers().get("Islands-Router").is_some();
830
831            let res_options = ResponseOptions::default();
832            let (meta_context, meta_output) = ServerMetaContext::new();
833
834            let additional_context = {
835                let meta_context = meta_context.clone();
836                let res_options = res_options.clone();
837                let req = Request::new(&req);
838                move || {
839                    provide_contexts(req, &meta_context, &res_options);
840                    add_context();
841
842                    if is_island_router_navigation {
843                        provide_context(IslandsRouterNavigation);
844                    }
845                }
846            };
847
848            let res = ActixResponse::from_app(
849                app_fn,
850                meta_output,
851                additional_context,
852                res_options,
853                stream_builder,
854                !is_island_router_navigation,
855            )
856            .await;
857
858            res.0
859        }
860    };
861    match method {
862        Method::Get => web::get().to(handler),
863        Method::Post => web::post().to(handler),
864        Method::Put => web::put().to(handler),
865        Method::Delete => web::delete().to(handler),
866        Method::Patch => web::patch().to(handler),
867    }
868}
869
870/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
871/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
872/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
873pub fn generate_route_list<IV>(
874    app_fn: impl Fn() -> IV + 'static + Send + Clone,
875) -> Vec<ActixRouteListing>
876where
877    IV: IntoView + 'static,
878{
879    generate_route_list_with_exclusions_and_ssg(app_fn, None).0
880}
881
882/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
883/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
884/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
885pub fn generate_route_list_with_ssg<IV>(
886    app_fn: impl Fn() -> IV + 'static + Send + Clone,
887) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
888where
889    IV: IntoView + 'static,
890{
891    generate_route_list_with_exclusions_and_ssg(app_fn, None)
892}
893
894/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
895/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
896/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
897/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format
898pub fn generate_route_list_with_exclusions<IV>(
899    app_fn: impl Fn() -> IV + 'static + Send + Clone,
900    excluded_routes: Option<Vec<String>>,
901) -> Vec<ActixRouteListing>
902where
903    IV: IntoView + 'static,
904{
905    generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
906}
907
908/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
909/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
910/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
911/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format
912pub fn generate_route_list_with_exclusions_and_ssg<IV>(
913    app_fn: impl Fn() -> IV + 'static + Send + Clone,
914    excluded_routes: Option<Vec<String>>,
915) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
916where
917    IV: IntoView + 'static,
918{
919    generate_route_list_with_exclusions_and_ssg_and_context(
920        app_fn,
921        excluded_routes,
922        || {},
923    )
924}
925
926trait ActixPath {
927    fn to_actix_path(&self) -> String;
928}
929
930impl ActixPath for Vec<PathSegment> {
931    fn to_actix_path(&self) -> String {
932        let mut path = String::new();
933        for segment in self.iter() {
934            // TODO trailing slash handling
935            let raw = segment.as_raw_str();
936            if !raw.is_empty() && !raw.starts_with('/') {
937                path.push('/');
938            }
939            match segment {
940                PathSegment::Static(s) => path.push_str(s),
941                PathSegment::Param(s) => {
942                    path.push('{');
943                    path.push_str(s);
944                    path.push('}');
945                }
946                PathSegment::Splat(s) => {
947                    path.push('{');
948                    path.push_str(s);
949                    path.push_str(":.*}");
950                }
951                PathSegment::Unit => {}
952                PathSegment::OptionalParam(_) => {
953                    #[cfg(feature = "tracing")]
954                    tracing::error!(
955                        "to_axum_path should only be called on expanded \
956                         paths, which do not have OptionalParam any longer"
957                    );
958                    Default::default()
959                }
960            }
961        }
962        path
963    }
964}
965
966#[derive(Clone, Debug, Default)]
967/// A route that this application can serve.
968pub struct ActixRouteListing {
969    path: String,
970    mode: SsrMode,
971    methods: Vec<leptos_router::Method>,
972    regenerate: Vec<RegenerationFn>,
973    exclude: bool,
974}
975
976trait IntoRouteListing: Sized {
977    fn into_route_listing(self) -> Vec<ActixRouteListing>;
978}
979
980impl IntoRouteListing for RouteListing {
981    fn into_route_listing(self) -> Vec<ActixRouteListing> {
982        self.path()
983            .to_vec()
984            .expand_optionals()
985            .into_iter()
986            .map(|path| {
987                let path = path.to_actix_path();
988                let path = if path.is_empty() {
989                    "/".to_string()
990                } else {
991                    path
992                };
993                let mode = self.mode();
994                let methods = self.methods().collect();
995                let regenerate = self.regenerate().into();
996                ActixRouteListing {
997                    path,
998                    mode: mode.clone(),
999                    methods,
1000                    regenerate,
1001                    exclude: false,
1002                }
1003            })
1004            .collect()
1005    }
1006}
1007
1008impl ActixRouteListing {
1009    /// Create a route listing from its parts.
1010    pub fn new(
1011        path: String,
1012        mode: SsrMode,
1013        methods: impl IntoIterator<Item = leptos_router::Method>,
1014        regenerate: impl Into<Vec<RegenerationFn>>,
1015    ) -> Self {
1016        Self {
1017            path,
1018            mode,
1019            methods: methods.into_iter().collect(),
1020            regenerate: regenerate.into(),
1021            exclude: false,
1022        }
1023    }
1024
1025    /// The path this route handles.
1026    pub fn path(&self) -> &str {
1027        &self.path
1028    }
1029
1030    /// The rendering mode for this path.
1031    pub fn mode(&self) -> SsrMode {
1032        self.mode.clone()
1033    }
1034
1035    /// The HTTP request methods this path can handle.
1036    pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
1037        self.methods.iter().copied()
1038    }
1039}
1040
1041/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
1042/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
1043/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
1044/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format.
1045/// Additional context will be provided to the app Element.
1046pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
1047    app_fn: impl Fn() -> IV + 'static + Send + Clone,
1048    excluded_routes: Option<Vec<String>>,
1049    additional_context: impl Fn() + 'static + Send + Clone,
1050) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
1051where
1052    IV: IntoView + 'static,
1053{
1054    let _ = any_spawner::Executor::init_tokio();
1055
1056    let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
1057    let (mock_meta, _) = ServerMetaContext::new();
1058    let routes = owner
1059        .with(|| {
1060            // stub out a path for now
1061            provide_context(RequestUrl::new(""));
1062            provide_context(ResponseOptions::default());
1063            provide_context(mock_meta);
1064            additional_context();
1065            RouteList::generate(&app_fn)
1066        })
1067        .unwrap_or_default();
1068
1069    let generator = StaticRouteGenerator::new(
1070        &routes,
1071        app_fn.clone(),
1072        additional_context.clone(),
1073    );
1074
1075    // Axum's Router defines Root routes as "/" not ""
1076    let mut routes = routes
1077        .into_inner()
1078        .into_iter()
1079        .flat_map(IntoRouteListing::into_route_listing)
1080        .collect::<Vec<_>>();
1081
1082    let routes = if routes.is_empty() {
1083        vec![ActixRouteListing::new(
1084            "/".to_string(),
1085            Default::default(),
1086            [leptos_router::Method::Get],
1087            vec![],
1088        )]
1089    } else {
1090        // Routes to exclude from auto generation
1091        if let Some(excluded_routes) = &excluded_routes {
1092            routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
1093        }
1094        routes
1095    };
1096
1097    let excluded =
1098        excluded_routes
1099            .into_iter()
1100            .flatten()
1101            .map(|path| ActixRouteListing {
1102                path,
1103                mode: Default::default(),
1104                methods: Vec::new(),
1105                regenerate: Vec::new(),
1106                exclude: true,
1107            });
1108
1109    (routes.into_iter().chain(excluded).collect(), generator)
1110}
1111
1112/// Allows generating any prerendered routes.
1113#[allow(clippy::type_complexity)]
1114pub struct StaticRouteGenerator(
1115    // this is here to keep the root owner alive for the duration
1116    // of the route generation, so that base context provided continues
1117    // to exist until it is dropped
1118    #[allow(dead_code)] Owner,
1119    Box<dyn FnOnce(&LeptosOptions) -> PinnedFuture<()> + Send>,
1120);
1121
1122impl StaticRouteGenerator {
1123    fn render_route<IV: IntoView + 'static>(
1124        path: String,
1125        app_fn: impl Fn() -> IV + Clone + Send + 'static,
1126        additional_context: impl Fn() + Clone + Send + 'static,
1127    ) -> impl Future<Output = (Owner, String)> {
1128        let (meta_context, meta_output) = ServerMetaContext::new();
1129        let additional_context = {
1130            let add_context = additional_context.clone();
1131            move || {
1132                let mock_req = test::TestRequest::with_uri(&path)
1133                    .insert_header(("Accept", "text/html"))
1134                    .to_http_request();
1135                let res_options = ResponseOptions::default();
1136                provide_contexts(
1137                    Request::new(&mock_req),
1138                    &meta_context,
1139                    &res_options,
1140                );
1141                add_context();
1142            }
1143        };
1144
1145        let (owner, stream) = leptos_integration_utils::build_response(
1146            app_fn.clone(),
1147            additional_context,
1148            async_stream_builder,
1149            false,
1150        );
1151
1152        let sc = owner.shared_context().unwrap();
1153
1154        async move {
1155            let stream = stream.await;
1156            while let Some(pending) = sc.await_deferred() {
1157                pending.await;
1158            }
1159
1160            let html = meta_output
1161                .inject_meta_context(stream)
1162                .await
1163                .collect::<String>()
1164                .await;
1165            (owner, html)
1166        }
1167    }
1168
1169    /// Creates a new static route generator from the given list of route definitions.
1170    pub fn new<IV>(
1171        routes: &RouteList,
1172        app_fn: impl Fn() -> IV + Clone + Send + 'static,
1173        additional_context: impl Fn() + Clone + Send + 'static,
1174    ) -> Self
1175    where
1176        IV: IntoView + 'static,
1177    {
1178        let owner = Owner::new();
1179        Self(owner.clone(), {
1180            let routes = routes.clone();
1181            Box::new(move |options| {
1182                let options = options.clone();
1183                let app_fn = app_fn.clone();
1184                let additional_context = additional_context.clone();
1185
1186                owner.with(|| {
1187                    additional_context();
1188                    Box::pin(ScopedFuture::new(routes.generate_static_files(
1189                        move |path: &ResolvedStaticPath| {
1190                            Self::render_route(
1191                                path.to_string(),
1192                                app_fn.clone(),
1193                                additional_context.clone(),
1194                            )
1195                        },
1196                        move |path: &ResolvedStaticPath,
1197                              owner: &Owner,
1198                              html: String| {
1199                            let options = options.clone();
1200                            let path = path.to_owned();
1201                            let response_options = owner.with(use_context);
1202                            async move {
1203                                write_static_route(
1204                                    &options,
1205                                    response_options,
1206                                    path.as_ref(),
1207                                    &html,
1208                                )
1209                                .await
1210                            }
1211                        },
1212                        was_404,
1213                    )))
1214                })
1215            })
1216        })
1217    }
1218
1219    /// Generates the routes.
1220    pub async fn generate(self, options: &LeptosOptions) {
1221        (self.1)(options).await
1222    }
1223}
1224
1225static STATIC_HEADERS: LazyLock<
1226    std::sync::RwLock<HashMap<String, ResponseOptions>>,
1227> = LazyLock::new(Default::default);
1228
1229fn was_404(owner: &Owner) -> bool {
1230    let resp = owner.with(|| expect_context::<ResponseOptions>());
1231    let status = resp.0.read().or_poisoned().status;
1232
1233    if let Some(status) = status {
1234        return status == StatusCode::NOT_FOUND;
1235    }
1236
1237    false
1238}
1239
1240fn static_path(options: &LeptosOptions, path: &str) -> String {
1241    use leptos_integration_utils::static_file_path;
1242
1243    // If the path ends with a trailing slash, we generate the path
1244    // as a directory with a index.html file inside.
1245    if path != "/" && path.ends_with("/") {
1246        static_file_path(options, &format!("{path}index"))
1247    } else {
1248        static_file_path(options, path)
1249    }
1250}
1251
1252async fn write_static_route(
1253    options: &LeptosOptions,
1254    response_options: Option<ResponseOptions>,
1255    path: &str,
1256    html: &str,
1257) -> Result<(), std::io::Error> {
1258    if let Some(options) = response_options {
1259        STATIC_HEADERS
1260            .write()
1261            .or_poisoned()
1262            .insert(path.to_string(), options);
1263    }
1264
1265    let path = static_path(options, path);
1266    let path = Path::new(&path);
1267    if let Some(path) = path.parent() {
1268        tokio::fs::create_dir_all(path).await?;
1269    }
1270    tokio::fs::write(path, &html).await?;
1271
1272    Ok(())
1273}
1274
1275fn handle_static_route<IV>(
1276    additional_context: impl Fn() + 'static + Clone + Send,
1277    app_fn: impl Fn() -> IV + Clone + Send + 'static,
1278    regenerate: Vec<RegenerationFn>,
1279) -> Route
1280where
1281    IV: IntoView + 'static,
1282{
1283    let handler = move |req: HttpRequest, data: Data<LeptosOptions>| {
1284        Box::pin({
1285            let app_fn = app_fn.clone();
1286            let additional_context = additional_context.clone();
1287            let regenerate = regenerate.clone();
1288            async move {
1289                let options = data.into_inner();
1290                let orig_path = req.uri().path();
1291                let path = static_path(&options, orig_path);
1292                let path = Path::new(&path);
1293                let exists = tokio::fs::try_exists(path).await.unwrap_or(false);
1294
1295                let (response_options, html) = if !exists {
1296                    let path = ResolvedStaticPath::new(orig_path);
1297
1298                    let (owner, html) = path
1299                        .build(
1300                            move |path: &ResolvedStaticPath| {
1301                                StaticRouteGenerator::render_route(
1302                                    path.to_string(),
1303                                    app_fn.clone(),
1304                                    additional_context.clone(),
1305                                )
1306                            },
1307                            move |path: &ResolvedStaticPath,
1308                                  owner: &Owner,
1309                                  html: String| {
1310                                let options = options.clone();
1311                                let path = path.to_owned();
1312                                let response_options = owner.with(use_context);
1313                                async move {
1314                                    write_static_route(
1315                                        &options,
1316                                        response_options,
1317                                        path.as_ref(),
1318                                        &html,
1319                                    )
1320                                    .await
1321                                }
1322                            },
1323                            was_404,
1324                            regenerate,
1325                        )
1326                        .await;
1327                    (owner.with(use_context::<ResponseOptions>), html)
1328                } else {
1329                    let headers = STATIC_HEADERS
1330                        .read()
1331                        .or_poisoned()
1332                        .get(orig_path)
1333                        .cloned();
1334                    (headers, None)
1335                };
1336
1337                // if html is Some(_), it means that `was_error_response` is true and we're not
1338                // actually going to cache this route, just return it as HTML
1339                //
1340                // this if for thing like 404s, where we do not want to cache an endless series of
1341                // typos (or malicious requests)
1342                let mut res = ActixResponse(match html {
1343                    Some(html) => {
1344                        HttpResponse::Ok().content_type("text/html").body(html)
1345                    }
1346                    None => match NamedFile::open(path) {
1347                        Ok(res) => res.into_response(&req),
1348                        Err(err) => HttpResponse::InternalServerError()
1349                            .body(err.to_string()),
1350                    },
1351                });
1352
1353                if let Some(options) = response_options {
1354                    res.extend_response(&options);
1355                }
1356
1357                res.0
1358            }
1359        })
1360    };
1361    web::get().to(handler)
1362}
1363
1364/// This trait allows one to pass a list of routes and a render function to Actix's router, letting us avoid
1365/// having to use wildcards or manually define all routes in multiple places.
1366pub trait LeptosRoutes {
1367    /// Adds routes to the Axum router that have either
1368    /// 1) been generated by `leptos_router`, or
1369    /// 2) handle a server function.
1370    fn leptos_routes<IV>(
1371        self,
1372        paths: Vec<ActixRouteListing>,
1373        app_fn: impl Fn() -> IV + Clone + Send + 'static,
1374    ) -> Self
1375    where
1376        IV: IntoView + 'static;
1377
1378    /// Adds routes to the Axum router that have either
1379    /// 1) been generated by `leptos_router`, or
1380    /// 2) handle a server function.
1381    ///
1382    /// Runs `additional_context` to provide additional data to the reactive system via context,
1383    /// when handling a route.
1384    fn leptos_routes_with_context<IV>(
1385        self,
1386        paths: Vec<ActixRouteListing>,
1387        additional_context: impl Fn() + 'static + Clone + Send,
1388        app_fn: impl Fn() -> IV + Clone + Send + 'static,
1389    ) -> Self
1390    where
1391        IV: IntoView + 'static;
1392}
1393
1394/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
1395/// to those paths to Leptos's renderer.
1396impl<T> LeptosRoutes for actix_web::App<T>
1397where
1398    T: ServiceFactory<
1399        ServiceRequest,
1400        Config = (),
1401        Error = Error,
1402        InitError = (),
1403    >,
1404{
1405    #[cfg_attr(
1406        feature = "tracing",
1407        tracing::instrument(level = "trace", fields(error), skip_all)
1408    )]
1409    fn leptos_routes<IV>(
1410        self,
1411        paths: Vec<ActixRouteListing>,
1412        app_fn: impl Fn() -> IV + Clone + Send + 'static,
1413    ) -> Self
1414    where
1415        IV: IntoView + 'static,
1416    {
1417        self.leptos_routes_with_context(paths, || {}, app_fn)
1418    }
1419
1420    #[cfg_attr(
1421        feature = "tracing",
1422        tracing::instrument(level = "trace", fields(error), skip_all)
1423    )]
1424    fn leptos_routes_with_context<IV>(
1425        self,
1426        paths: Vec<ActixRouteListing>,
1427        additional_context: impl Fn() + 'static + Clone + Send,
1428        app_fn: impl Fn() -> IV + Clone + Send + 'static,
1429    ) -> Self
1430    where
1431        IV: IntoView + 'static,
1432    {
1433        let mut router = self;
1434
1435        let excluded = paths
1436            .iter()
1437            .filter(|&p| p.exclude)
1438            .map(|p| p.path.as_str())
1439            .collect::<HashSet<_>>();
1440
1441        // register server functions first to allow for wildcard route in Leptos's Router
1442        for (path, _) in server_fn::actix::server_fn_paths() {
1443            if !excluded.contains(path) {
1444                let additional_context = additional_context.clone();
1445                let handler =
1446                    handle_server_fns_with_context(additional_context);
1447                router = router.route(path, handler);
1448            }
1449        }
1450
1451        // register routes defined in Leptos's Router
1452        for listing in paths.iter().filter(|p| !p.exclude) {
1453            let path = listing.path();
1454            let mode = listing.mode();
1455
1456            for method in listing.methods() {
1457                let additional_context = additional_context.clone();
1458                let additional_context_and_method = move || {
1459                    provide_context(method);
1460                    additional_context();
1461                };
1462                router = if matches!(listing.mode(), SsrMode::Static(_)) {
1463                    router.route(
1464                        path,
1465                        handle_static_route(
1466                            additional_context_and_method.clone(),
1467                            app_fn.clone(),
1468                            listing.regenerate.clone(),
1469                        ),
1470                    )
1471                } else {
1472                    router
1473                        .route(path, web::head().to(HttpResponse::Ok))
1474                        .route(
1475                            path,
1476                            match mode {
1477                                SsrMode::OutOfOrder => {
1478                                    render_app_to_stream_with_context(
1479                                        additional_context_and_method.clone(),
1480                                        app_fn.clone(),
1481                                        method,
1482                                    )
1483                                }
1484                                SsrMode::PartiallyBlocked => {
1485                                    render_app_to_stream_with_context_and_replace_blocks(
1486                                        additional_context_and_method.clone(),
1487                                        app_fn.clone(),
1488                                        method,
1489                                        true,
1490                                    )
1491                                }
1492                                SsrMode::InOrder => {
1493                                    render_app_to_stream_in_order_with_context(
1494                                        additional_context_and_method.clone(),
1495                                        app_fn.clone(),
1496                                        method,
1497                                    )
1498                                }
1499                                SsrMode::Async => render_app_async_with_context(
1500                                    additional_context_and_method.clone(),
1501                                    app_fn.clone(),
1502                                    method,
1503                                ),
1504                                _ => unreachable!()
1505                            },
1506                        )
1507                };
1508            }
1509        }
1510
1511        router
1512    }
1513}
1514
1515/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
1516/// to those paths to Leptos's renderer.
1517impl LeptosRoutes for &mut ServiceConfig {
1518    #[cfg_attr(
1519        feature = "tracing",
1520        tracing::instrument(level = "trace", fields(error), skip_all)
1521    )]
1522    fn leptos_routes<IV>(
1523        self,
1524        paths: Vec<ActixRouteListing>,
1525        app_fn: impl Fn() -> IV + Clone + Send + 'static,
1526    ) -> Self
1527    where
1528        IV: IntoView + 'static,
1529    {
1530        self.leptos_routes_with_context(paths, || {}, app_fn)
1531    }
1532
1533    #[cfg_attr(
1534        feature = "tracing",
1535        tracing::instrument(level = "trace", fields(error), skip_all)
1536    )]
1537    fn leptos_routes_with_context<IV>(
1538        self,
1539        paths: Vec<ActixRouteListing>,
1540        additional_context: impl Fn() + 'static + Clone + Send,
1541        app_fn: impl Fn() -> IV + Clone + Send + 'static,
1542    ) -> Self
1543    where
1544        IV: IntoView + 'static,
1545    {
1546        let mut router = self;
1547
1548        let excluded = paths
1549            .iter()
1550            .filter(|&p| p.exclude)
1551            .map(|p| p.path.as_str())
1552            .collect::<HashSet<_>>();
1553
1554        // register server functions first to allow for wildcard route in Leptos's Router
1555        for (path, _) in server_fn::actix::server_fn_paths() {
1556            if !excluded.contains(path) {
1557                let additional_context = additional_context.clone();
1558                let handler =
1559                    handle_server_fns_with_context(additional_context);
1560                router = router.route(path, handler);
1561            }
1562        }
1563
1564        // register routes defined in Leptos's Router
1565        for listing in paths.iter().filter(|p| !p.exclude) {
1566            let path = listing.path();
1567            let mode = listing.mode();
1568
1569            for method in listing.methods() {
1570                if matches!(listing.mode(), SsrMode::Static(_)) {
1571                    router = router.route(
1572                        path,
1573                        handle_static_route(
1574                            additional_context.clone(),
1575                            app_fn.clone(),
1576                            listing.regenerate.clone(),
1577                        ),
1578                    )
1579                } else {
1580                    router = router.route(
1581                            path,
1582                            match mode {
1583                                SsrMode::OutOfOrder => {
1584                                    render_app_to_stream_with_context(
1585                                        additional_context.clone(),
1586                                        app_fn.clone(),
1587                                        method,
1588                                    )
1589                                }
1590                                SsrMode::PartiallyBlocked => {
1591                                    render_app_to_stream_with_context_and_replace_blocks(
1592                                        additional_context.clone(),
1593                                        app_fn.clone(),
1594                                        method,
1595                                        true,
1596                                    )
1597                                }
1598                                SsrMode::InOrder => {
1599                                    render_app_to_stream_in_order_with_context(
1600                                        additional_context.clone(),
1601                                        app_fn.clone(),
1602                                        method,
1603                                    )
1604                                }
1605                                SsrMode::Async => render_app_async_with_context(
1606                                    additional_context.clone(),
1607                                    app_fn.clone(),
1608                                    method,
1609                                ),
1610                                _ => unreachable!()
1611                            },
1612                        );
1613                }
1614            }
1615        }
1616
1617        router
1618    }
1619}
1620
1621/// A helper to make it easier to use Actix extractors in server functions.
1622///
1623/// It is generic over some type `T` that implements [`FromRequest`] and can
1624/// therefore be used in an extractor. The compiler can often infer this type.
1625///
1626/// Any error that occurs during extraction is converted to a [`ServerFnError`].
1627///
1628/// ```rust
1629/// use leptos::prelude::*;
1630///
1631/// #[server]
1632/// pub async fn extract_connection_info() -> Result<String, ServerFnError> {
1633///     use actix_web::dev::ConnectionInfo;
1634///     use leptos_actix::*;
1635///
1636///     // this can be any type you can use an Actix extractor with, as long as
1637///     // it works on the head, not the body of the request
1638///     let info: ConnectionInfo = extract().await?;
1639///
1640///     // do something with the data
1641///
1642///     Ok(format!("{info:?}"))
1643/// }
1644/// ```
1645pub async fn extract<T>() -> Result<T, ServerFnErrorErr>
1646where
1647    T: actix_web::FromRequest,
1648    <T as FromRequest>::Error: Display,
1649{
1650    let req = use_context::<Request>().ok_or_else(|| {
1651        ServerFnErrorErr::ServerError(
1652            "HttpRequest should have been provided via context".to_string(),
1653        )
1654    })?;
1655
1656    SendWrapper::new(async move {
1657        T::extract(&req)
1658            .await
1659            .map_err(|e| ServerFnErrorErr::ServerError(e.to_string()))
1660    })
1661    .await
1662}