#![forbid(unsafe_code)]
use actix_http::header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER};
use actix_web::{
body::BoxBody,
dev::{ServiceFactory, ServiceRequest},
http::header,
web::{Payload, ServiceConfig},
*,
};
use futures::{stream::once, Stream, StreamExt};
use http::StatusCode;
use hydration_context::SsrSharedContext;
use leptos::{
context::{provide_context, use_context},
reactive_graph::{computed::ScopedFuture, owner::Owner},
IntoView, *,
};
use leptos_integration_utils::{
BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
};
use leptos_meta::ServerMetaContext;
use leptos_router::{
components::provide_server_redirect, location::RequestUrl, PathSegment,
RouteList, RouteListing, SsrMode, StaticDataMap, StaticMode, *,
};
use parking_lot::RwLock;
use send_wrapper::SendWrapper;
use server_fn::{
redirect::REDIRECT_HEADER, request::actix::ActixRequest, ServerFnError,
};
use std::{
fmt::{Debug, Display},
ops::{Deref, DerefMut},
sync::Arc,
};
#[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)]
pub struct Request(SendWrapper<HttpRequest>);
impl Request {
pub fn new(req: &HttpRequest) -> Self {
Self(SendWrapper::new(req.clone()))
}
pub fn into_inner(self) -> HttpRequest {
self.0.take()
}
}
impl Deref for Request {
type Target = HttpRequest;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Request {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[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);
}
}
struct ActixResponse(HttpResponse);
impl ExtendResponse for ActixResponse {
type ResponseOptions = ResponseOptions;
fn from_stream(
stream: impl Stream<Item = String> + Send + 'static,
) -> Self {
ActixResponse(
HttpResponse::Ok()
.content_type("text/html")
.streaming(stream.map(|chunk| {
Ok(web::Bytes::from(chunk)) as Result<web::Bytes>
})),
)
}
fn extend_response(&mut self, res_options: &Self::ResponseOptions) {
let mut res_options = res_options.0.write();
let headers = self.0.headers_mut();
for (key, value) in std::mem::take(&mut res_options.headers) {
headers.append(key, value);
}
if let Some(status) = res_options.status {
*self.0.status_mut() = status;
}
}
fn set_default_content_type(&mut self, content_type: &str) {
let headers = self.0.headers_mut();
if !headers.contains_key(header::CONTENT_TYPE) {
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_str(content_type).unwrap(),
);
}
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn redirect(path: &str) {
if let (Some(req), Some(res)) =
(use_context::<Request>(), 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 {
let msg = "Couldn't retrieve either Parts or ResponseOptions while \
trying to redirect().";
#[cfg(feature = "tracing")]
tracing::warn!("{}", &msg);
#[cfg(not(feature = "tracing"))]
eprintln!("{}", &msg);
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn handle_server_fns() -> Route {
handle_server_fns_with_context(|| {})
}
#[cfg_attr(
feature = "tracing",
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();
let method = req.method();
if let Some(mut service) =
server_fn::actix::get_server_fn_service(path, method)
{
let owner = Owner::new();
owner
.with(|| {
ScopedFuture::new(async move {
additional_context();
provide_context(Request::new(&req));
let res_options = ResponseOptions::default();
provide_context(res_options.clone());
let accepts_html = req
.headers()
.get(ACCEPT)
.and_then(|v| v.to_str().ok())
.map(|v| v.contains("text/html"))
.unwrap_or(false);
let referrer = req.headers().get(REFERER).cloned();
let mut res = ActixResponse(
service
.0
.run(ActixRequest::from((req, payload)))
.await
.take(),
);
if accepts_html {
if let Some(referrer) = referrer {
let has_location =
res.0.headers().get(LOCATION).is_some();
if !has_location {
*res.0.status_mut() = StatusCode::FOUND;
res.0
.headers_mut()
.insert(LOCATION, referrer);
}
}
}
res.extend_response(&res_options);
res.0
})
})
.await
} 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()
))
}
}
})
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
) -> Route
where
IV: IntoView + 'static,
{
render_app_to_stream_with_context(|| {}, app_fn, method)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
) -> Route
where
IV: IntoView + 'static,
{
render_app_to_stream_in_order_with_context(|| {}, app_fn, method)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
) -> Route
where
IV: IntoView + 'static,
{
render_app_async_with_context(|| {}, app_fn, method)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
) -> Route
where
IV: IntoView + 'static,
{
render_app_to_stream_with_context_and_replace_blocks(
additional_context,
app_fn,
method,
false,
)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
replace_blocks: bool,
) -> Route
where
IV: IntoView + 'static,
{
_ = replace_blocks; handle_response(method, additional_context, app_fn, |app, chunks| {
Box::pin(async move {
Box::pin(app.to_html_stream_out_of_order().chain(chunks()))
as PinnedStream<String>
})
})
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
) -> Route
where
IV: IntoView + 'static,
{
handle_response(method, additional_context, app_fn, |app, chunks| {
Box::pin(async move {
Box::pin(app.to_html_stream_in_order().chain(chunks()))
as PinnedStream<String>
})
})
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
) -> Route
where
IV: IntoView + 'static,
{
handle_response(method, additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = app.to_html_stream_in_order().collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
})
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn provide_contexts(
req: Request,
meta_context: &ServerMetaContext,
res_options: &ResponseOptions,
) {
let path = leptos_corrected_path(&req);
provide_context(RequestUrl::new(&path));
provide_context(meta_context.clone());
provide_context(res_options.clone());
provide_context(req);
provide_server_redirect(redirect);
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
}
}
fn handle_response<IV>(
method: Method,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
) -> PinnedFuture<PinnedStream<String>>,
) -> Route
where
IV: IntoView + 'static,
{
let handler = move |req: HttpRequest| {
let app_fn = app_fn.clone();
let add_context = additional_context.clone();
async move {
let res_options = ResponseOptions::default();
let (meta_context, meta_output) = ServerMetaContext::new();
let additional_context = {
let meta_context = meta_context.clone();
let res_options = res_options.clone();
let req = Request::new(&req);
move || {
provide_contexts(req, &meta_context, &res_options);
add_context();
}
};
let res = ActixResponse::from_app(
app_fn,
meta_output,
additional_context,
res_options,
stream_builder,
)
.await;
res.0
}
};
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 fn generate_route_list<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> Vec<ActixRouteListing>
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<ActixRouteListing>, 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<ActixRouteListing>
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<ActixRouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg_and_context(
app_fn,
excluded_routes,
|| {},
)
}
trait ActixPath {
fn to_actix_path(&self) -> String;
}
impl ActixPath for &[PathSegment] {
fn to_actix_path(&self) -> String {
let mut path = String::new();
for segment in self.iter() {
let raw = segment.as_raw_str();
if !raw.is_empty() && !raw.starts_with('/') {
path.push('/');
}
match segment {
PathSegment::Static(s) => path.push_str(s),
PathSegment::Param(s) => {
path.push('{');
path.push_str(s);
path.push('}');
}
PathSegment::Splat(s) => {
path.push('{');
path.push_str(s);
path.push_str(":.*}");
}
PathSegment::Unit => {}
}
}
path
}
}
#[derive(Clone, Debug, Default)]
pub struct ActixRouteListing {
path: String,
mode: SsrMode,
methods: Vec<leptos_router::Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
}
impl From<RouteListing> for ActixRouteListing {
fn from(value: RouteListing) -> Self {
let path = value.path().to_actix_path();
let path = if path.is_empty() {
"/".to_string()
} else {
path
};
let mode = value.mode();
let methods = value.methods().collect();
let static_mode = value.into_static_parts();
Self {
path,
mode,
methods,
static_mode,
}
}
}
impl ActixRouteListing {
pub fn new(
path: String,
mode: SsrMode,
methods: impl IntoIterator<Item = leptos_router::Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
) -> Self {
Self {
path,
mode,
methods: methods.into_iter().collect(),
static_mode,
}
}
pub fn path(&self) -> &str {
&self.path
}
pub fn mode(&self) -> SsrMode {
self.mode
}
pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
self.methods.iter().copied()
}
#[inline(always)]
pub fn static_mode(&self) -> Option<StaticMode> {
self.static_mode.as_ref().map(|n| n.0)
}
}
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<ActixRouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
let _ = any_spawner::Executor::init_tokio();
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
let (mock_meta, _) = ServerMetaContext::new();
let routes = owner
.with(|| {
provide_context(RequestUrl::new(""));
provide_context(ResponseOptions::default());
provide_context(mock_meta);
additional_context();
RouteList::generate(&app_fn)
})
.unwrap_or_default();
let mut routes = routes
.into_inner()
.into_iter()
.map(ActixRouteListing::from)
.collect::<Vec<_>>();
(
if routes.is_empty() {
vec![ActixRouteListing::new(
"/".to_string(),
Default::default(),
[leptos_router::Method::Get],
None,
)]
} else {
if let Some(excluded_routes) = excluded_routes {
routes
.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
},
StaticDataMap::new(), )
}
pub enum DataResponse<T> {
Data(T),
Response(actix_web::dev::Response<BoxBody>),
}
pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
paths: Vec<ActixRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
fn leptos_routes_with_context<IV>(
self,
paths: Vec<ActixRouteListing>,
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 = (),
>,
{
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes<IV>(
self,
paths: Vec<ActixRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
self.leptos_routes_with_context(paths, || {}, app_fn)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes_with_context<IV>(
self,
paths: Vec<ActixRouteListing>,
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() {
_ = static_mode;
todo!()
} else {
router.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
additional_context_and_method.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
),
},
)
};
}
}
router
}
}
impl LeptosRoutes for &mut ServiceConfig {
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes<IV>(
self,
paths: Vec<ActixRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
self.leptos_routes_with_context(paths, || {}, app_fn)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes_with_context<IV>(
self,
paths: Vec<ActixRouteListing>,
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(
additional_context.clone(),
app_fn.clone(),
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
additional_context.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
additional_context.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
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::<Request>().ok_or_else(|| {
ServerFnError::new("HttpRequest should have been provided via context")
})?;
T::extract(&req)
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}