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 }