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}