diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs
index ab935e4485..6989756fcc 100644
--- a/examples/action-form-error-handling/src/app.rs
+++ b/examples/action-form-error-handling/src/app.rs
@@ -59,12 +59,11 @@ fn HomePage() -> impl IntoView {
.to_string())
>
{value}
-
-
+
}
}
diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs
index 54b9311c81..6301861618 100644
--- a/integrations/actix/src/lib.rs
+++ b/integrations/actix/src/lib.rs
@@ -20,12 +20,13 @@ use actix_web::{
use futures::{Stream, StreamExt};
use leptos::{
leptos_server::{server_fn_by_path, Payload},
- server_fn::Encoding,
+ server_fn::{Encoding, query_to_errors},
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::*;
@@ -35,7 +36,7 @@ use std::{
fmt::{Debug, Display},
future::Future,
pin::Pin,
- sync::{Arc, OnceLock},
+ sync::Arc,
};
#[cfg(debug_assertions)]
use tracing::instrument;
@@ -160,8 +161,6 @@ pub fn handle_server_fns() -> Route {
handle_server_fns_with_context(|| {})
}
-static REGEX_CELL: OnceLock = 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].
@@ -200,20 +199,6 @@ 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
@@ -245,6 +230,8 @@ 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 =
@@ -254,104 +241,81 @@ 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
- for (k, v) in res_parts.headers.clone() {
- res.append_header((k, v));
- }
-
- leptos::logging::log!(
- "server fn serialized = {serialized:?}"
- );
+ let _count = res_parts
+ .headers
+ .clone()
+ .into_iter()
+ .map(|(k, v)| {
+ res.append_header((k, v));
+ })
+ .count();
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) => {
- 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}"
- );
-
+ let url = req
+ .headers()
+ .get(header::REFERER)
+ .and_then(|referrer| {
+ referrer_to_url(referrer, fn_name.as_str())
+ });
+
+ if let Some(url) = url {
HttpResponse::SeeOther()
.insert_header((
header::LOCATION,
- url.with_server_fn_error(
+ url.with_server_fn(
&e,
fn_name.as_str(),
)
@@ -366,7 +330,7 @@ pub fn handle_server_fns_with_context(
}
}
};
-
+ leptos::logging::log!("done serverfn with status {}", res.status());
// clean up the scope
runtime.dispose();
res
@@ -804,13 +768,12 @@ fn provide_contexts(req: &HttpRequest, res_options: ResponseOptions) {
provide_context(MetaContext::new());
provide_context(res_options);
provide_context(req.clone());
- // TODO: Fix
- // if let Some(query) = req.uri().query() {
- // leptos::logging::log!("query = {query}");
- // provide_context(query_to_responses(
- // query
- // ));
- // }
+ if let Some(query) = req.uri().query() {
+ leptos::logging::log!("query = {query}");
+ provide_context(query_to_errors(
+ query
+ ));
+ }
provide_server_redirect(redirect);
#[cfg(feature = "nonce")]
diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs
index 57b3390575..d442d3780a 100644
--- a/integrations/axum/src/lib.rs
+++ b/integrations/axum/src/lib.rs
@@ -388,7 +388,7 @@ async fn handle_server_fns_inner(
.status(StatusCode::SEE_OTHER)
.header(
header::LOCATION,
- referer.with_server_fn_error(&e, fn_name.as_str()).as_str(),
+ referer.with_server_fn(&e, fn_name.as_str()).as_str(),
)
.body(Default::default())
} else {
diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs
index fca64f2fec..0ddd7a628d 100644
--- a/integrations/utils/src/lib.rs
+++ b/integrations/utils/src/lib.rs
@@ -1,13 +1,12 @@
use futures::{Stream, StreamExt};
+use http::HeaderValue;
use leptos::{
- nonce::use_nonce, server_fn::ServerFnUrlResponse, use_context, RuntimeId,
- ServerFnError,
+ nonce::use_nonce, server_fn::error::ServerFnUrlError, 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;
@@ -165,63 +164,31 @@ pub async fn build_async_response(
format!("{head}{buf}{tail}")
}
-pub fn referrer_to_url(referer: &str, fn_name: &str) -> Url {
+pub fn referrer_to_url(referer: &HeaderValue, fn_name: &str) -> Option {
Url::parse(
&Regex::new(&format!(r"(?:\?|&)?server_fn_error_{fn_name}=[^&]+"))
.unwrap()
- .replace(referer, ""),
+ .replace(referer.to_str().ok()?, ""),
)
- .expect("Could not parse URL")
+ .ok()
}
-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;
+pub trait WithServerFn {
+ fn with_server_fn(self, error: &ServerFnError, fn_name: &str) -> Self;
}
-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::::from_error(fn_name, error.clone()),
- fn_name,
- )
- }
-
- 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,
- 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!")
+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(),
- );
+ );
- url
+ self
+ }
}
diff --git a/integrations/viz/src/lib.rs b/integrations/viz/src/lib.rs
index 04828ef1a4..7a5c48a043 100644
--- a/integrations/viz/src/lib.rs
+++ b/integrations/viz/src/lib.rs
@@ -310,7 +310,7 @@ async fn handle_server_fns_inner(
.status(StatusCode::SEE_OTHER)
.header(
header::LOCATION,
- url.with_server_fn_error(&e, fn_name.as_str()).as_str(),
+ url.with_server_fn(&e, fn_name.as_str()).as_str(),
)
.body(Default::default())
} else {
diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs
index 5c32d8a1fc..b48bf2622a 100644
--- a/leptos/src/lib.rs
+++ b/leptos/src/lib.rs
@@ -187,7 +187,7 @@ pub use leptos_server::{
create_server_multi_action, Action, MultiAction, ServerFn, ServerFnError,
ServerFnErrorErr,
};
-pub use server_fn::{self, query_to_responses, ServerFn as _};
+pub use server_fn::{self, query_to_errors, ServerFn as _};
mod error_boundary;
pub use error_boundary::*;
mod animated_show;
@@ -352,6 +352,3 @@ where
(self)(props).into_view()
}
}
-
-#[doc(hidden)]
-pub const WASM_LOADED_NAME: &'static str = "leptos_client_wasm_loaded";
diff --git a/router/src/components/form.rs b/router/src/components/form.rs
index 1509bfe5e6..b1bbb70eef 100644
--- a/router/src/components/form.rs
+++ b/router/src/components/form.rs
@@ -2,12 +2,7 @@ use crate::{
hooks::has_router, use_navigate, use_resolved_path, NavigateOptions,
ToHref, Url,
};
-use leptos::{
- html::form,
- logging::*,
- server_fn::ServerFnUrlResponse,
- *,
-};
+use leptos::{html::form, logging::*, server_fn::error::ServerFnUrlError, *};
use serde::{de::DeserializeOwned, Serialize};
use std::{collections::HashSet, error::Error, rc::Rc};
use wasm_bindgen::{JsCast, UnwrapThrowExt};
@@ -464,16 +459,17 @@ where
let action_url = effect_action_url.clone();
Effect::new_isomorphic(move |_| {
- let results = use_context::>>();
- if let Some(result) = results
- .map(|results| {
- results
+ let errors = use_context::>();
+ if let Some(url_error) =
+ errors
+ .map(|errors| {
+ errors
.into_iter()
- .find(|e| effect_action_url.contains(e.name()))
+ .find(|e| effect_action_url.contains(e.fn_name()))
})
- .flatten()
- {
- value.try_set(Some(result.get()));
+ .flatten() {
+ leptos::logging::log!("In iso effect with error = {url_error:?}");
+ value.try_set(Some(Err(url_error.error().clone())));
}
});
@@ -482,10 +478,10 @@ where
wasm_has_loaded.set(true);
});
- view! {
+ view!{
Self {
+ Self {
+ internal_fn_name: fn_name,
+ internal_error: error,
+ }
+ }
+
+ /// TODO: Write documentation
+ pub fn error(&self) -> &ServerFnError {
+ &self.internal_error
+ }
+
+ /// TODO: Add docs
+ pub fn fn_name(&self) -> &str {
+ &self.internal_fn_name.as_ref()
+ }
+}
+
+impl From for ServerFnError {
+ fn from(error: ServerFnUrlError) -> Self {
+ error.internal_error
+ }
+}
+
+impl From for ServerFnErrorErr {
+ fn from(error: ServerFnUrlError) -> Self {
+ error.internal_error.into()
+ }
+}
+
impl core::fmt::Display for ServerFnError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs
index b53d718561..df4d54dabf 100644
--- a/server_fn/src/lib.rs
+++ b/server_fn/src/lib.rs
@@ -82,6 +82,7 @@
// used by the macro
#[doc(hidden)]
pub use const_format;
+use error::ServerFnUrlError;
// used by the macro
#[cfg(feature = "ssr")]
#[doc(hidden)]
@@ -93,10 +94,10 @@ use quote::TokenStreamExt;
// used by the macro
#[doc(hidden)]
pub use serde;
-use serde::{de::DeserializeOwned, Serialize, Deserialize};
+use serde::{de::DeserializeOwned, Serialize};
pub use server_fn_macro_default::server;
use url::Url;
-use std::{future::Future, pin::Pin, str::FromStr, collections::HashSet, hash::Hash, cmp::Eq};
+use std::{future::Future, pin::Pin, str::FromStr, collections::HashSet};
#[cfg(any(feature = "ssr", doc))]
use syn::parse_quote;
// used by the macro
@@ -599,41 +600,6 @@ where
}
}
-/// TODO: Write Documentation
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
-pub struct ServerFnUrlResponse {
- data: Result,
- fn_name: String,
-}
-
-impl ServerFnUrlResponse {
- /// TODO: Write Documentation
- pub fn from_error(fn_name: &str, error: ServerFnError) -> Self {
- Self {
- fn_name: fn_name.to_owned(),
- data: Err(error),
- }
- }
-
- /// TODO: Add docs
- pub fn new(fn_name: &str, data: T) -> Self {
- Self {
- data: Ok(data),
- fn_name: fn_name.to_owned(),
- }
- }
-
- /// TODO: Write documentation
- pub fn get(&self) -> Result {
- self.data.clone()
- }
-
- /// TODO: Add docs
- pub fn name(&self) -> &str {
- &self.fn_name.as_ref()
- }
-}
-
// Lazily initialize the client to be reused for all server function calls.
#[cfg(any(all(not(feature = "ssr"), not(target_arch = "wasm32")), doc))]
static CLIENT: once_cell::sync::Lazy =
@@ -657,7 +623,7 @@ fn get_server_url() -> &'static str {
}
#[doc(hidden)]
-pub fn query_to_responses(query: &str) -> HashSet> {
+pub fn query_to_errors(query: &str) -> HashSet {
// Url::parse needs an full absolute URL to parse correctly.
// Since this function is only interested in the query pairs,
// the specific scheme and domain do not matter.
@@ -666,8 +632,8 @@ pub fn query_to_responses(query: &str)
.query_pairs()
.into_iter()
.filter_map(|(k, v)| {
- if k.starts_with("server_fn_response_") {
- serde_qs::from_str::<'_, ServerFnUrlResponse>(v.as_ref()).ok()
+ if k.starts_with("server_fn_error_") {
+ serde_qs::from_str::<'_, ServerFnUrlError>(v.as_ref()).ok()
} else {
None
}