#![forbid(unsafe_code)]
use actix_http::header::{HeaderName, HeaderValue, ACCEPT};
use actix_web::{
body::BoxBody,
dev::{ServiceFactory, ServiceRequest},
http::header,
web::{Payload, ServiceConfig},
*,
};
use futures::{Stream, StreamExt};
use http::StatusCode;
use leptos::{
ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement,
*,
};
use leptos_integration_utils::{build_async_response, html_parts_separated};
use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
use regex::Regex;
use server_fn::{redirect::REDIRECT_HEADER, request::actix::ActixRequest};
use std::{
fmt::{Debug, Display},
future::Future,
pin::Pin,
sync::Arc,
};
#[cfg(debug_assertions)]
use tracing::instrument;
#[derive(Debug, Clone, Default)]
pub struct ResponseParts {
pub headers: header::HeaderMap,
pub status: Option<StatusCode>,
}
impl ResponseParts {
pub fn insert_header(
&mut self,
key: header::HeaderName,
value: header::HeaderValue,
) {
self.headers.insert(key, value);
}
pub fn append_header(
&mut self,
key: header::HeaderName,
value: header::HeaderValue,
) {
self.headers.append(key, value);
}
}
#[derive(Debug, Clone, Default)]
pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
impl ResponseOptions {
pub fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write();
*writable = parts
}
pub fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.status = Some(status);
}
pub fn insert_header(
&self,
key: header::HeaderName,
value: header::HeaderValue,
) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.headers.insert(key, value);
}
pub fn append_header(
&self,
key: header::HeaderName,
value: header::HeaderValue,
) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.headers.append(key, value);
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn redirect(path: &str) {
if let (Some(req), Some(res)) = (
use_context::<HttpRequest>(),
use_context::<ResponseOptions>(),
) {
res.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path)
.expect("Failed to create HeaderValue"),
);
let accepts_html = req
.headers()
.get(ACCEPT)
.and_then(|v| v.to_str().ok())
.map(|v| v.contains("text/html"))
.unwrap_or(false);
if accepts_html {
res.set_status(StatusCode::FOUND);
} else {
res.insert_header(
HeaderName::from_static(REDIRECT_HEADER),
HeaderValue::from_str("").unwrap(),
);
}
} else {
tracing::warn!(
"Couldn't retrieve either Parts or ResponseOptions while trying \
to redirect()."
);
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn handle_server_fns() -> Route {
handle_server_fns_with_context(|| {})
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn handle_server_fns_with_context(
additional_context: impl Fn() + 'static + Clone + Send,
) -> Route {
web::to(move |req: HttpRequest, payload: Payload| {
let additional_context = additional_context.clone();
async move {
let additional_context = additional_context.clone();
let path = req.path();
if let Some(mut service) =
server_fn::actix::get_server_fn_service(path)
{
let runtime = create_runtime();
additional_context();
provide_context(req.clone());
let res_parts = ResponseOptions::default();
provide_context(res_parts.clone());
let mut res = service
.0
.run(ActixRequest::from((req, payload)))
.await
.take();
if let Some(status) = res_parts.0.read().status {
*res.status_mut() = status;
}
let headers = res.headers_mut();
let mut res_parts = res_parts.0.write();
for location in res_parts.headers.remove(header::LOCATION) {
headers.insert(header::LOCATION, location);
}
for (k, v) in std::mem::take(&mut res_parts.headers) {
headers.append(k, v);
}
runtime.dispose();
res
} else {
HttpResponse::BadRequest().body(format!(
"Could not find a server function at the route {:?}. \
\n\nIt's likely that either
1. The API prefix you specify in the `#[server]` \
macro doesn't match the prefix at which your server \
function handler is mounted, or \n2. You are on a \
platform that doesn't support automatic server function \
registration and you need to call \
ServerFn::register_explicit() on the server function \
type, somewhere in your `main` function.",
req.path()
))
}
}
})
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_to_stream<IV>(
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
render_app_to_stream_with_context(options, || {}, app_fn, method)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_to_stream_in_order<IV>(
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
render_app_to_stream_in_order_with_context(options, || {}, app_fn, method)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_async<IV>(
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
render_app_async_with_context(options, || {}, app_fn, method)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_to_stream_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
render_app_to_stream_with_context_and_replace_blocks(
options,
additional_context,
app_fn,
method,
false,
)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
options: LeptosOptions,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + 'static,
method: Method,
replace_blocks: bool,
) -> Route
where
IV: IntoView,
{
let handler = move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let res_options = ResponseOptions::default();
async move {
let app = {
let app_fn = app_fn.clone();
let res_options = res_options.clone();
move || {
provide_contexts(&req, res_options);
(app_fn)().into_view()
}
};
stream_app(
&options,
app,
res_options,
additional_context,
replace_blocks,
)
.await
}
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_to_stream_in_order_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
let handler = move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let res_options = ResponseOptions::default();
async move {
let app = {
let app_fn = app_fn.clone();
let res_options = res_options.clone();
move || {
provide_contexts(&req, res_options);
(app_fn)().into_view()
}
};
stream_app_in_order(&options, app, res_options, additional_context)
.await
}
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_async_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
let handler = move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let res_options = ResponseOptions::default();
async move {
let app = {
let app_fn = app_fn.clone();
let res_options = res_options.clone();
move || {
provide_contexts(&req, res_options);
(app_fn)().into_view()
}
};
render_app_async_helper(
&options,
app,
res_options,
additional_context,
)
.await
}
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn provide_contexts(req: &HttpRequest, res_options: ResponseOptions) {
let path = leptos_corrected_path(req);
let integration = ServerIntegration { path };
provide_context(RouterIntegrationContext::new(integration));
provide_context(MetaContext::new());
provide_context(res_options);
provide_context(req.clone());
provide_server_redirect(redirect);
#[cfg(feature = "nonce")]
leptos::nonce::provide_nonce();
}
fn leptos_corrected_path(req: &HttpRequest) -> String {
let path = req.path();
let query = req.query_string();
if query.is_empty() {
"http://leptos".to_string() + path
} else {
"http://leptos".to_string() + path + "?" + query
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
async fn stream_app(
options: &LeptosOptions,
app: impl FnOnce() -> View + 'static,
res_options: ResponseOptions,
additional_context: impl Fn() + 'static + Clone + Send,
replace_blocks: bool,
) -> HttpResponse<BoxBody> {
let (stream, runtime) =
render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
app,
move || generate_head_metadata_separated().1.into(),
additional_context,
replace_blocks
);
build_stream_response(options, res_options, stream, runtime).await
}
#[cfg_attr(any(debug_assertions), instrument(level = "trace", skip_all,))]
async fn stream_app_in_order(
options: &LeptosOptions,
app: impl FnOnce() -> View + 'static,
res_options: ResponseOptions,
additional_context: impl Fn() + 'static + Clone + Send,
) -> HttpResponse<BoxBody> {
let (stream, runtime) =
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
move || {
generate_head_metadata_separated().1.into()
},
additional_context,
);
build_stream_response(options, res_options, stream, runtime).await
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
async fn build_stream_response(
options: &LeptosOptions,
res_options: ResponseOptions,
stream: impl Stream<Item = String> + 'static,
runtime: RuntimeId,
) -> HttpResponse {
let mut stream = Box::pin(stream);
let first_app_chunk = stream.next().await.unwrap_or_default();
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>().as_ref());
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })
.chain(
futures::stream::once(async move { first_app_chunk })
.chain(stream),
)
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
);
let first_chunk = stream.next().await;
let second_chunk = stream.next().await;
let res_options = res_options.0.read();
let (status, headers) = (res_options.status, res_options.headers.clone());
let status = status.unwrap_or_default();
let complete_stream =
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()])
.chain(stream)
.chain(
futures::stream::once(async move {
runtime.dispose();
tail.to_string()
})
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
);
let mut res = HttpResponse::Ok()
.content_type("text/html")
.streaming(complete_stream);
for (key, value) in headers.into_iter() {
res.headers_mut().append(key, value);
}
let res_status = res.status_mut();
*res_status = status;
res
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
async fn render_app_async_helper(
options: &LeptosOptions,
app: impl FnOnce() -> View + 'static,
res_options: ResponseOptions,
additional_context: impl Fn() + 'static + Clone + Send,
) -> HttpResponse<BoxBody> {
let (stream, runtime) =
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
move || "".into(),
additional_context,
);
let html = build_async_response(stream, options, runtime).await;
let res_options = res_options.0.read();
let (status, headers) = (res_options.status, res_options.headers.clone());
let status = status.unwrap_or_default();
let mut res = HttpResponse::Ok().content_type("text/html").body(html);
for (key, value) in headers.into_iter() {
res.headers_mut().append(key, value);
}
let res_status = res.status_mut();
*res_status = status;
res
}
pub fn generate_route_list<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, None).0
}
pub fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, None)
}
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
}
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg_and_context(
app_fn,
excluded_routes,
|| {},
)
}
pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
additional_context: impl Fn() + 'static + Clone,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
let (mut routes, static_data_map) =
leptos_router::generate_route_list_inner_with_context(
app_fn,
additional_context,
);
let wildcard_re = Regex::new(r"\*.*").unwrap();
let capture_re = Regex::new(r":((?:[^.,/]+)+)[^/]?").unwrap();
routes = routes
.into_iter()
.map(|listing| {
let path = listing.path();
if path.is_empty() {
return RouteListing::new(
"/".to_string(),
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
);
}
RouteListing::new(
listing.path(),
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
)
})
.map(|listing| {
let path = wildcard_re
.replace_all(listing.path(), "{tail:.*}")
.to_string();
let path = capture_re.replace_all(&path, "{$1}").to_string();
RouteListing::new(
path,
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
)
})
.collect::<Vec<_>>();
(
if routes.is_empty() {
vec![RouteListing::new(
"/",
"",
Default::default(),
[Method::Get],
None,
)]
} else {
if let Some(excluded_routes) = excluded_routes {
routes
.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
},
static_data_map,
)
}
pub enum DataResponse<T> {
Data(T),
Response(actix_web::dev::Response<BoxBody>),
}
fn handle_static_response<'a, IV>(
path: &'a str,
options: &'a LeptosOptions,
app_fn: &'a (impl Fn() -> IV + Clone + Send + 'static),
additional_context: &'a (impl Fn() + 'static + Clone + Send),
res: StaticResponse,
) -> Pin<Box<dyn Future<Output = HttpResponse<String>> + 'a>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
match res {
StaticResponse::ReturnResponse {
body,
status,
content_type,
} => {
let mut res = HttpResponse::new(match status {
StaticStatusCode::Ok => StatusCode::OK,
StaticStatusCode::NotFound => StatusCode::NOT_FOUND,
StaticStatusCode::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
});
if let Some(v) = content_type {
res.headers_mut().insert(
HeaderName::from_static("content-type"),
HeaderValue::from_static(v),
);
}
res.set_body(body)
}
StaticResponse::RenderDynamic => {
handle_static_response(
path,
options,
app_fn,
additional_context,
render_dynamic(
path,
options,
app_fn.clone(),
additional_context.clone(),
)
.await,
)
.await
}
StaticResponse::RenderNotFound => {
handle_static_response(
path,
options,
app_fn,
additional_context,
not_found_page(
tokio::fs::read_to_string(not_found_path(options))
.await,
),
)
.await
}
StaticResponse::WriteFile { body, path } => {
if let Some(path) = path.parent() {
if let Err(e) = std::fs::create_dir_all(path) {
tracing::error!(
"encountered error {} writing directories {}",
e,
path.display()
);
}
}
if let Err(e) = std::fs::write(&path, &body) {
tracing::error!(
"encountered error {} writing file {}",
e,
path.display()
);
}
handle_static_response(
path.to_str().unwrap(),
options,
app_fn,
additional_context,
StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
)
.await
}
}
})
}
fn static_route<IV>(
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
method: Method,
mode: StaticMode,
) -> Route
where
IV: IntoView + 'static,
{
match mode {
StaticMode::Incremental => {
let handler = move |req: HttpRequest| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
handle_static_response(
req.path(),
&options,
&app_fn,
&additional_context,
incremental_static_route(
tokio::fs::read_to_string(static_file_path(
&options,
req.path(),
))
.await,
),
)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
StaticMode::Upfront => {
let handler = move |req: HttpRequest| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
handle_static_response(
req.path(),
&options,
&app_fn,
&additional_context,
upfront_static_route(
tokio::fs::read_to_string(static_file_path(
&options,
req.path(),
))
.await,
),
)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
}
}
pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<RouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<RouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
}
impl<T> LeptosRoutes for actix_web::App<T>
where
T: ServiceFactory<
ServiceRequest,
Config = (),
Error = Error,
InitError = (),
>,
{
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<RouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
self.leptos_routes_with_context(options, paths, || {}, app_fn)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<RouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let mut router = self;
for (path, _) in server_fn::actix::server_fn_paths() {
let additional_context = additional_context.clone();
let handler = handle_server_fns_with_context(additional_context);
router = router.route(path, handler);
}
for listing in paths.iter() {
let path = listing.path();
let mode = listing.mode();
for method in listing.methods() {
let additional_context = additional_context.clone();
let additional_context_and_method = move || {
provide_context(method);
additional_context();
};
router = if let Some(static_mode) = listing.static_mode() {
router.route(
path,
static_route(
options.clone(),
app_fn.clone(),
additional_context_and_method.clone(),
method,
static_mode,
),
)
} else {
router.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
options.clone(),
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
options.clone(),
additional_context_and_method.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
options.clone(),
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
options.clone(),
additional_context_and_method.clone(),
app_fn.clone(),
method,
),
},
)
};
}
}
router
}
}
impl LeptosRoutes for &mut ServiceConfig {
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<RouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
self.leptos_routes_with_context(options, paths, || {}, app_fn)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<RouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let mut router = self;
for (path, _) in server_fn::actix::server_fn_paths() {
let additional_context = additional_context.clone();
let handler = handle_server_fns_with_context(additional_context);
router = router.route(path, handler);
}
for listing in paths.iter() {
let path = listing.path();
let mode = listing.mode();
for method in listing.methods() {
router = router.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
options.clone(),
additional_context.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
method,
),
},
);
}
}
router
}
}
pub async fn extract<T>() -> Result<T, ServerFnError>
where
T: actix_web::FromRequest,
<T as FromRequest>::Error: Display,
{
let req = use_context::<HttpRequest>().ok_or_else(|| {
ServerFnError::new("HttpRequest should have been provided via context")
})?;
T::extract(&req)
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}