Skip to content

Commit

Permalink
Work on handling server fns
Browse files Browse the repository at this point in the history
  • Loading branch information
SleeplessOne1917 committed Jan 13, 2024
1 parent 5b056a7 commit 1472aaa
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 142 deletions.
3 changes: 2 additions & 1 deletion examples/action-form-error-handling/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,12 @@ fn HomePage() -> impl IntoView {
.to_string())
>
{value}
</ErrorBoundary>
<ActionForm action=do_something_action class="form">
<label>Should error: <input type="checkbox" name="should_error"/></label>
<button type="submit">Submit</button>
</ActionForm>
</ErrorBoundary>

}
}

Expand Down
157 changes: 97 additions & 60 deletions integrations/actix/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@ use actix_web::{
use futures::{Stream, StreamExt};
use leptos::{
leptos_server::{server_fn_by_path, Payload},
server_fn::{Encoding, query_to_errors},
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,
referrer_to_url, WithServerFn,
build_async_response, html_parts_separated, referrer_to_url, WithServerFn,
};
use leptos_meta::*;
use leptos_router::*;
Expand All @@ -36,7 +35,7 @@ use std::{
fmt::{Debug, Display},
future::Future,
pin::Pin,
sync::Arc,
sync::{Arc, OnceLock},
};
#[cfg(debug_assertions)]
use tracing::instrument;
Expand Down Expand Up @@ -161,6 +160,8 @@ pub fn handle_server_fns() -> Route {
handle_server_fns_with_context(|| {})
}

static REGEX_CELL: OnceLock<Regex> = OnceLock::new();

/// An Actix [struct@Route](actix_web::Route) that listens for `GET` or `POST` requests with
/// Leptos server function arguments in the URL (`GET`) or body (`POST`),
/// runs the server function if found, and returns the resulting [HttpResponse].
Expand Down Expand Up @@ -199,6 +200,20 @@ pub fn handle_server_fns_with_context(
if let Some(server_fn) = server_fn_by_path(fn_name.as_str()) {
let body_ref: &[u8] = &body;

let wasm_loaded = REGEX_CELL
.get_or_init(|| {
Regex::new(&format!(
"{WASM_LOADED_NAME}=(true|false)"
))
.expect("Could not parse wasm loaded regex")
})
.captures(std::str::from_utf8(body_ref).unwrap_or(""))
.map_or(true, |capture| {
capture
.get(1)
.map_or(true, |s| s.as_str() == "true")
});

let runtime = create_runtime();

// Add additional info to the context of the server function
Expand Down Expand Up @@ -230,8 +245,6 @@ pub fn handle_server_fns_with_context(
Encoding::GetJSON | Encoding::GetCBOR => query,
};

leptos::logging::log!("In server fn before resp with data = {data:?}");

let res = match server_fn.call((), data).await {
Ok(serialized) => {
let res_options =
Expand All @@ -241,81 +254,104 @@ pub fn handle_server_fns_with_context(
HttpResponse::Ok();
let res_parts = res_options.0.write();

// if accept_header isn't set to one of these, it's a form submit
// redirect back to the referer if not redirect has been set
if accept_header != Some("application/json")
&& accept_header
!= Some("application/x-www-form-urlencoded")
&& accept_header != Some("application/cbor")
{
leptos::logging::log!("Will redirect for form submit");
// Location will already be set if redirect() has been used
let has_location_set = res_parts
.headers
.get(header::LOCATION)
.is_some();
if !has_location_set {
let referer = req
.headers()
.get(header::REFERER)
.and_then(|value| value.to_str().ok())
.unwrap_or("/");
res = HttpResponse::SeeOther();
res.insert_header((
header::LOCATION,
referer,
))
.content_type("application/json");
}
}
// Override StatusCode if it was set in a Resource or Element
if let Some(status) = res_parts.status {
res.status(status);
}

// Use provided ResponseParts headers if they exist
let _count = res_parts
.headers
.clone()
.into_iter()
.map(|(k, v)| {
res.append_header((k, v));
})
.count();
for (k, v) in res_parts.headers.clone() {
res.append_header((k, v));
}

leptos::logging::log!(
"server fn serialized = {serialized:?}"
);

match serialized {
Payload::Binary(data) => {
leptos::logging::log!("serverfn return bin = {data:?}");
res.content_type("application/cbor");
res.body(Bytes::from(data))
}
Payload::Url(data) => {
leptos::logging::log!("serverfn return url = {data:?}");
res.content_type(
"application/x-www-form-urlencoded",
);

// if accept_header isn't set to one of these, it's a form submit
// redirect back to the referer if not redirect has been set
if !(wasm_loaded
|| matches!(
accept_header,
Some(
"application/json"
| "application/\
x-www-form-urlencoded"
| "application/cbor"
)
))
{
// Location will already be set if redirect() has been used
let has_location_set = res_parts
.headers
.get(header::LOCATION)
.is_some();
if !has_location_set {
let referer = req
.headers()
.get(header::REFERER)
.and_then(|value| {
value.to_str().ok()
})
.unwrap_or("/");
let location = referrer_to_url(
referer, &fn_name,
);
leptos::logging::log!(
"Form submit referrer = \
{referer:?}"
);
res = HttpResponse::SeeOther();
res.insert_header((
header::LOCATION,
location
.with_server_fn_success(
&data, &fn_name,
)
.as_str(),
))
.content_type("application/json");
}
}

res.body(data)
}
Payload::Json(data) => {
leptos::logging::log!("serverfn return json = {data:?}");
res.content_type("application/json");
res.body(data)
}
}
}
Err(e) => {
let url = req
.headers()
.get(header::REFERER)
.and_then(|referrer| {
referrer_to_url(referrer, fn_name.as_str())
});

if let Some(url) = url {
if !wasm_loaded {
leptos::logging::log!(
"In err with WASM loaded"
);
let referer = req
.headers()
.get(header::REFERER)
.and_then(|value| value.to_str().ok())
.unwrap_or("/");
let url = referrer_to_url(referer, &fn_name);

leptos::logging::log!(
"In err with WASM loaded and url = {url}"
);

HttpResponse::SeeOther()
.insert_header((
header::LOCATION,
url.with_server_fn(
url.with_server_fn_error(
&e,
fn_name.as_str(),
)
Expand All @@ -330,7 +366,7 @@ pub fn handle_server_fns_with_context(
}
}
};
leptos::logging::log!("done serverfn with status {}", res.status());

// clean up the scope
runtime.dispose();
res
Expand Down Expand Up @@ -768,12 +804,13 @@ fn provide_contexts(req: &HttpRequest, res_options: ResponseOptions) {
provide_context(MetaContext::new());
provide_context(res_options);
provide_context(req.clone());
if let Some(query) = req.uri().query() {
leptos::logging::log!("query = {query}");
provide_context(query_to_errors(
query
));
}
// TODO: Fix
// if let Some(query) = req.uri().query() {
// leptos::logging::log!("query = {query}");
// provide_context(query_to_responses(
// query
// ));
// }

provide_server_redirect(redirect);
#[cfg(feature = "nonce")]
Expand Down
2 changes: 1 addition & 1 deletion integrations/axum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ async fn handle_server_fns_inner(
.status(StatusCode::SEE_OTHER)
.header(
header::LOCATION,
referer.with_server_fn(&e, fn_name.as_str()).as_str(),
referer.with_server_fn_error(&e, fn_name.as_str()).as_str(),
)
.body(Default::default())
} else {
Expand Down
73 changes: 53 additions & 20 deletions integrations/utils/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use futures::{Stream, StreamExt};
use http::HeaderValue;
use leptos::{
nonce::use_nonce, server_fn::error::ServerFnUrlError, use_context,
RuntimeId, ServerFnError,
nonce::use_nonce, server_fn::ServerFnUrlResponse, use_context, RuntimeId,
ServerFnError,
};
use leptos_config::LeptosOptions;
use leptos_meta::MetaContext;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{cmp::Eq, hash::Hash};
use url::Url;

extern crate tracing;
Expand Down Expand Up @@ -164,31 +165,63 @@ pub async fn build_async_response(
format!("{head}<body{body_meta}>{buf}{tail}")
}

pub fn referrer_to_url(referer: &HeaderValue, fn_name: &str) -> Option<Url> {
pub fn referrer_to_url(referer: &str, fn_name: &str) -> Url {
Url::parse(
&Regex::new(&format!(r"(?:\?|&)?server_fn_error_{fn_name}=[^&]+"))
.unwrap()
.replace(referer.to_str().ok()?, ""),
.replace(referer, ""),
)
.ok()
.expect("Could not parse URL")
}

pub trait WithServerFn {
fn with_server_fn(self, error: &ServerFnError, fn_name: &str) -> Self;
pub trait WithServerFn<'de, T>
where
T: Clone + Deserialize<'de> + Hash + Eq + Serialize,
{
fn with_server_fn_error(self, error: &ServerFnError, fn_name: &str)
-> Self;
fn with_server_fn_success(self, data: &T, fn_name: &str) -> Self;
}

impl WithServerFn for Url {
fn with_server_fn(mut self, error: &ServerFnError, fn_name: &str) -> Self {
self.query_pairs_mut().append_pair(
format!("server_fn_error_{fn_name}").as_str(),
serde_qs::to_string(&ServerFnUrlError::new(
fn_name.to_owned(),
error.to_owned(),
))
.expect("Could not serialize server fn error!")
.as_str(),
);
impl<'de, T> WithServerFn<'de, T> for Url
where
T: Clone + Deserialize<'de> + Hash + Eq + Serialize,
{
fn with_server_fn_error(
self,
error: &ServerFnError,
fn_name: &str,
) -> Self {
modify_server_fn_response(
self,
ServerFnUrlResponse::<T>::from_error(fn_name, error.clone()),
fn_name,
)
}

self
fn with_server_fn_success(self, data: &T, fn_name: &str) -> Self {
modify_server_fn_response(
self,
ServerFnUrlResponse::new(fn_name, data.clone()),
fn_name,
)
}
}

fn modify_server_fn_response<'de, T>(
mut url: Url,
res: ServerFnUrlResponse<T>,
fn_name: &str,
) -> Url
where
T: Clone + Deserialize<'de> + Hash + Eq + Serialize,
{
url.query_pairs_mut().append_pair(
format!("server_fn_response_{fn_name}").as_str(),
serde_qs::to_string(&res)
.expect("Could not serialize server fn response!")
.as_str(),
);

url
}
2 changes: 1 addition & 1 deletion integrations/viz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ async fn handle_server_fns_inner(
.status(StatusCode::SEE_OTHER)
.header(
header::LOCATION,
url.with_server_fn(&e, fn_name.as_str()).as_str(),
url.with_server_fn_error(&e, fn_name.as_str()).as_str(),
)
.body(Default::default())
} else {
Expand Down
5 changes: 4 additions & 1 deletion leptos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ pub use leptos_server::{
create_server_multi_action, Action, MultiAction, ServerFn, ServerFnError,
ServerFnErrorErr,
};
pub use server_fn::{self, query_to_errors, ServerFn as _};
pub use server_fn::{self, query_to_responses, ServerFn as _};
mod error_boundary;
pub use error_boundary::*;
mod animated_show;
Expand Down Expand Up @@ -352,3 +352,6 @@ where
(self)(props).into_view()
}
}

#[doc(hidden)]
pub const WASM_LOADED_NAME: &'static str = "leptos_client_wasm_loaded";
Loading

0 comments on commit 1472aaa

Please sign in to comment.