#![forbid(unsafe_code)]
use actix_web::{
body::BoxBody,
dev::{ServiceFactory, ServiceRequest},
http::header,
web::Bytes,
*,
};
use futures::{Stream, StreamExt};
use http::StatusCode;
use leptos::{
leptos_server::{server_fn_by_path, Payload},
server_fn::Encoding,
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 std::{fmt::Display, future::Future, sync::Arc};
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(cx: leptos::Scope, path: &str) {
if let Some(response_options) = use_context::<ResponseOptions>(cx) {
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path)
.expect("Failed to create HeaderValue"),
);
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn handle_server_fns() -> Route {
handle_server_fns_with_context(|_cx| {})
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn handle_server_fns_with_context(
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> Route {
web::to(
move |req: HttpRequest, params: web::Path<String>, body: web::Bytes| {
let additional_context = additional_context.clone();
async move {
let additional_context = additional_context.clone();
let path = params.into_inner();
let accept_header = req
.headers()
.get("Accept")
.and_then(|value| value.to_str().ok());
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
let body_ref: &[u8] = &body;
let runtime = create_runtime();
let (cx, disposer) = raw_scope_and_disposer(runtime);
additional_context(cx);
let res_options = ResponseOptions::default();
provide_context(cx, req.clone());
provide_context(cx, res_options.clone());
if req
.headers()
.get("Content-Type")
.and_then(|value| value.to_str().ok())
.map(|value| {
value.starts_with("multipart/form-data; boundary=")
})
== Some(true)
{
provide_context(cx, body.clone());
}
let query = req.query_string().as_bytes();
let data = match &server_fn.encoding() {
Encoding::Url | Encoding::Cbor => body_ref,
Encoding::GetJSON | Encoding::GetCBOR => query,
};
let res = match server_fn.call(cx, data).await {
Ok(serialized) => {
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
let mut res: HttpResponseBuilder;
let res_parts = res_options.0.write();
if accept_header == Some("application/json")
|| accept_header
== Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
{
res = HttpResponse::Ok();
}
else {
let referer = req
.headers()
.get("Referer")
.and_then(|value| value.to_str().ok())
.unwrap_or("/");
res = HttpResponse::SeeOther();
res.insert_header(("Location", referer))
.content_type("application/json");
};
if let Some(status) = res_parts.status {
res.status(status);
}
let _count = res_parts
.headers
.clone()
.into_iter()
.map(|(k, v)| {
res.append_header((k, v));
})
.count();
match serialized {
Payload::Binary(data) => {
res.content_type("application/cbor");
res.body(Bytes::from(data))
}
Payload::Url(data) => {
res.content_type(
"application/x-www-form-urlencoded",
);
res.body(data)
}
Payload::Json(data) => {
res.content_type("application/json");
res.body(data)
}
}
}
Err(e) => HttpResponse::InternalServerError().body(
serde_json::to_string(&e)
.unwrap_or_else(|_| e.to_string()),
),
};
disposer.dispose();
runtime.dispose();
res
} else {
HttpResponse::BadRequest().body(format!(
"Could not find a server function at the route {:?}. \
\n\nIt's likely that 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(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
render_app_to_stream_with_context(options, |_cx| {}, 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(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
render_app_to_stream_in_order_with_context(
options,
|_cx| {},
app_fn,
method,
)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_async<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
render_app_async_with_context(options, |_cx| {}, 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(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> 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(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> 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 |cx| {
provide_contexts(cx, &req, res_options);
(app_fn)(cx).into_view(cx)
}
};
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(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> 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 |cx| {
provide_contexts(cx, &req, res_options);
(app_fn)(cx).into_view(cx)
}
};
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(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> 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 |cx| {
provide_contexts(cx, &req, res_options);
(app_fn)(cx).into_view(cx)
}
};
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(
cx: leptos::Scope,
req: &HttpRequest,
res_options: ResponseOptions,
) {
let path = leptos_corrected_path(req);
let integration = ServerIntegration { path };
provide_context(cx, RouterIntegrationContext::new(integration));
provide_context(cx, MetaContext::new());
provide_context(cx, res_options);
provide_context(cx, req.clone());
provide_server_redirect(cx, move |path| redirect(cx, path));
}
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(leptos::Scope) -> View + 'static,
res_options: ResponseOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
replace_blocks: bool,
) -> HttpResponse<BoxBody> {
let (stream, runtime, scope) =
render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
app,
move |cx| generate_head_metadata_separated(cx).1.into(),
additional_context,
replace_blocks
);
build_stream_response(options, res_options, stream, runtime, scope).await
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
async fn stream_app_in_order(
options: &LeptosOptions,
app: impl FnOnce(leptos::Scope) -> View + 'static,
res_options: ResponseOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> HttpResponse<BoxBody> {
let (stream, runtime, scope) =
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
move |cx| {
generate_head_metadata_separated(cx).1.into()
},
additional_context,
);
build_stream_response(options, res_options, stream, runtime, scope).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,
scope: ScopeId,
) -> HttpResponse {
let cx = leptos::Scope { runtime, id: scope };
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>(cx).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),
)
.chain(futures::stream::once(async move {
runtime.dispose();
tail.to_string()
}))
.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);
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(leptos::Scope) -> View + 'static,
res_options: ResponseOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> HttpResponse<BoxBody> {
let (stream, runtime, scope) =
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, scope).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 FnOnce(leptos::Scope) -> IV + 'static,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions(app_fn, None)
}
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl FnOnce(leptos::Scope) -> IV + 'static,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
let mut routes = leptos_router::generate_route_list_inner(app_fn);
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.mode(),
listing.methods(),
);
}
RouteListing::new(listing.path(), listing.mode(), listing.methods())
})
.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.mode(), listing.methods())
})
.collect::<Vec<_>>();
if routes.is_empty() {
vec![RouteListing::new("/", Default::default(), [Method::Get])]
} else {
if let Some(excluded_routes) = excluded_routes {
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
}
}
pub enum DataResponse<T> {
Data(T),
Response(actix_web::dev::Response<BoxBody>),
}
pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> 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(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> 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(leptos::Scope) -> 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(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let mut router = self;
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<F, E>(
cx: leptos::Scope,
f: F,
) -> Result<<<F as Extractor<E>>::Future as Future>::Output, ServerFnError>
where
F: Extractor<E>,
E: actix_web::FromRequest,
<E as actix_web::FromRequest>::Error: Display,
<F as Extractor<E>>::Future: Future,
{
let req = use_context::<actix_web::HttpRequest>(cx)
.expect("HttpRequest should have been provided via context");
let input = if let Some(body) = use_context::<Bytes>(cx) {
let (_, mut payload) = actix_http::h1::Payload::create(false);
payload.unread_data(body);
E::from_request(&req, &mut dev::Payload::from(payload))
} else {
E::extract(&req)
}
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
Ok(f.call(input).await)
}
pub trait Extractor<T> {
type Future;
fn call(self, args: T) -> Self::Future;
}
macro_rules! factory_tuple ({ $($param:ident)* } => {
impl<Func, Fut, $($param,)*> Extractor<($($param,)*)> for Func
where
Func: FnOnce($($param),*) -> Fut + Clone + 'static,
Fut: Future,
{
type Future = Fut;
#[inline]
#[allow(non_snake_case)]
fn call(self, ($($param,)*): ($($param,)*)) -> Self::Future {
(self)($($param,)*)
}
}
});
factory_tuple! {}
factory_tuple! { A }
factory_tuple! { A B }
factory_tuple! { A B C }
factory_tuple! { A B C D }
factory_tuple! { A B C D E }
factory_tuple! { A B C D E F }
factory_tuple! { A B C D E F G }
factory_tuple! { A B C D E F G H }
factory_tuple! { A B C D E F G H I }
factory_tuple! { A B C D E F G H I J }
factory_tuple! { A B C D E F G H I J K }
factory_tuple! { A B C D E F G H I J K L }
factory_tuple! { A B C D E F G H I J K L M }
factory_tuple! { A B C D E F G H I J K L M N }
factory_tuple! { A B C D E F G H I J K L M N O }
factory_tuple! { A B C D E F G H I J K L M N O P }