diff --git a/examples/action-form-error-handling/Cargo.toml b/examples/action-form-error-handling/Cargo.toml new file mode 100644 index 0000000000..80500d4821 --- /dev/null +++ b/examples/action-form-error-handling/Cargo.toml @@ -0,0 +1,90 @@ +[package] +name = "action-form-error-handling" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +actix-files = { version = "0.6", optional = true } +actix-web = { version = "4", optional = true, features = ["macros"] } +console_error_panic_hook = "0.1" +cfg-if = "1" +http = { version = "0.2", optional = true } +leptos = { path = "../../leptos" } +leptos_meta = { path = "../../meta" } +leptos_actix = { path = "../../integrations/actix", optional = true } +leptos_router = { path = "../../router" } +wasm-bindgen = "0.2" +serde = { version = "1", features = ["derive"] } + +[features] +csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] +hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] +ssr = [ + "dep:actix-files", + "dep:actix-web", + "dep:leptos_actix", + "leptos/ssr", + "leptos_meta/ssr", + "leptos_router/ssr", +] + +# Defines a size-optimized profile for the WASM bundle in release mode +[profile.wasm-release] +inherits = "release" +opt-level = 'z' +lto = true +codegen-units = 1 +panic = "abort" + +[package.metadata.leptos] +# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name +output-name = "leptos_start" +# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup. +site-root = "target/site" +# The site-root relative folder where all compiled output (JS, WASM and CSS) is written +# Defaults to pkg +site-pkg-dir = "pkg" +# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to //app.css +style-file = "style/main.scss" +# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup. +site-addr = "127.0.0.1:3000" +# The port to use for automatic reload monitoring +reload-port = 3001 +# [Optional] Command to use when running end2end tests. It will run in the end2end dir. +# [Windows] for non-WSL use "npx.cmd playwright test" +# This binary name can be checked in Powershell with Get-Command npx +end2end-cmd = "npx playwright test" +end2end-dir = "end2end" +# The browserlist query used for optimizing the CSS. +browserquery = "defaults" +# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head +watch = false +# The environment Leptos will run in, usually either "DEV" or "PROD" +env = "DEV" +# The features to use when compiling the bin target +# +# Optional. Can be over-ridden with the command line parameter --bin-features +bin-features = ["ssr"] + +# If the --no-default-features flag should be used when compiling the bin target +# +# Optional. Defaults to false. +bin-default-features = false + +# The features to use when compiling the lib target +# +# Optional. Can be over-ridden with the command line parameter --lib-features +lib-features = ["hydrate"] + +# If the --no-default-features flag should be used when compiling the lib target +# +# Optional. Defaults to false. +lib-default-features = false + +# The profile to use for the lib target when compiling for release +# +# Optional. Defaults to "release". +lib-profile-release = "wasm-release" diff --git a/examples/action-form-error-handling/Makefile.toml b/examples/action-form-error-handling/Makefile.toml new file mode 100644 index 0000000000..a1f669ca28 --- /dev/null +++ b/examples/action-form-error-handling/Makefile.toml @@ -0,0 +1,8 @@ +extend = [ + { path = "../cargo-make/main.toml" }, + { path = "../cargo-make/cargo-leptos.toml" }, +] + +[env] + +CLIENT_PROCESS_NAME = "action_form_error_handling" diff --git a/examples/action-form-error-handling/README.md b/examples/action-form-error-handling/README.md new file mode 100644 index 0000000000..e85b41b2ce --- /dev/null +++ b/examples/action-form-error-handling/README.md @@ -0,0 +1,68 @@ + + + Leptos Logo + + +# Leptos Starter Template + +This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool. + +## Creating your template repo + +If you don't have `cargo-leptos` installed you can install it with + +`cargo install cargo-leptos` + +Then run + +`cargo leptos new --git leptos-rs/start` + +to generate a new project template (you will be prompted to enter a project name). + +`cd {projectname}` + +to go to your newly created project. + +Of course, you should explore around the project structure, but the best place to start with your application code is in `src/app.rs`. + +## Running your project + +`cargo leptos watch` +By default, you can access your local project at `http://localhost:3000` + +## Installing Additional Tools + +By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools. + +1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly +2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly +3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future) +4. `npm install -g sass` - install `dart-sass` (should be optional in future) + +## Executing a Server on a Remote Machine Without the Toolchain +After running a `cargo leptos build --release` the minimum files needed are: + +1. The server binary located in `target/server/release` +2. The `site` directory and all files within located in `target/site` + +Copy these files to your remote server. The directory structure should be: +```text +leptos_start +site/ +``` +Set the following environment variables (updating for your project as needed): +```sh +export LEPTOS_OUTPUT_NAME="leptos_start" +export LEPTOS_SITE_ROOT="site" +export LEPTOS_SITE_PKG_DIR="pkg" +export LEPTOS_SITE_ADDR="127.0.0.1:3000" +export LEPTOS_RELOAD_PORT="3001" +``` +Finally, run the server binary. + +## Notes about CSR and Trunk: +Although it is not recommended, you can also run your project without server integration using the feature `csr` and `trunk serve`: + +`trunk serve --open --features csr` + +This may be useful for integrating external tools which require a static site, e.g. `tauri`. diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs new file mode 100644 index 0000000000..6989756fcc --- /dev/null +++ b/examples/action-form-error-handling/src/app.rs @@ -0,0 +1,90 @@ +use leptos::*; +use leptos_meta::*; +use leptos_router::*; + +#[component] +pub fn App() -> impl IntoView { + // Provides context that manages stylesheets, titles, meta tags, etc. + provide_meta_context(); + + view! { + // injects a stylesheet into the document + // id=leptos means cargo-leptos will hot-reload this stylesheet + + + // sets the document title + + + // content for this welcome page + <Router> + <main id="app"> + <Routes> + <Route path="" view=HomePage/> + <Route path="/*any" view=NotFound/> + </Routes> + </main> + </Router> + } +} + +#[server] +async fn do_something(should_error: Option<String>) -> Result<String, ServerFnError> { + if should_error.is_none() { + Ok(String::from("Successful submit")) + } else { + Err(ServerFnError::ServerError(String::from( + "You got an error!", + ))) + } +} + +/// Renders the home page of your application. +#[component] +fn HomePage() -> impl IntoView { + let do_something_action = Action::<DoSomething, _>::server(); + let value = Signal::derive(move || do_something_action.value().get().unwrap_or_else(|| Ok(String::new()))); + + Effect::new_isomorphic(move |_| { + logging::log!("Got value = {:?}", value.get()); + }); + + view! { + <h1>"Test the action form!"</h1> + <ErrorBoundary fallback=move |error| format!("{:#?}", error + .get() + .into_iter() + .next() + .unwrap() + .1.into_inner() + .to_string()) + > + {value} + <ActionForm action=do_something_action class="form"> + <label>Should error: <input type="checkbox" name="should_error"/></label> + <button type="submit">Submit</button> + </ActionForm> + </ErrorBoundary> + } +} + +/// 404 - Not Found +#[component] +fn NotFound() -> impl IntoView { + // set an HTTP status code 404 + // this is feature gated because it can only be done during + // initial server-side rendering + // if you navigate to the 404 page subsequently, the status + // code will not be set because there is not a new HTTP request + // to the server + #[cfg(feature = "ssr")] + { + // this can be done inline because it's synchronous + // if it were async, we'd use a server function + let resp = expect_context::<leptos_actix::ResponseOptions>(); + resp.set_status(actix_web::http::StatusCode::NOT_FOUND); + } + + view! { + <h1>"Not Found"</h1> + } +} diff --git a/examples/action-form-error-handling/src/lib.rs b/examples/action-form-error-handling/src/lib.rs new file mode 100644 index 0000000000..f1b7478302 --- /dev/null +++ b/examples/action-form-error-handling/src/lib.rs @@ -0,0 +1,18 @@ +pub mod app; +use cfg_if::cfg_if; + +cfg_if! { +if #[cfg(feature = "hydrate")] { + + use wasm_bindgen::prelude::wasm_bindgen; + + #[wasm_bindgen] + pub fn hydrate() { + use app::*; + + console_error_panic_hook::set_once(); + + leptos::mount_to_body(App); + } +} +} diff --git a/examples/action-form-error-handling/src/main.rs b/examples/action-form-error-handling/src/main.rs new file mode 100644 index 0000000000..d9cbb052e8 --- /dev/null +++ b/examples/action-form-error-handling/src/main.rs @@ -0,0 +1,53 @@ +#[cfg(feature = "ssr")] +#[actix_web::main] +async fn main() -> std::io::Result<()> { + use actix_files::Files; + use actix_web::*; + use leptos::*; + use leptos_actix::{generate_route_list, LeptosRoutes}; + use action_form_error_handling::app::*; + + let conf = get_configuration(None).await.unwrap(); + let addr = conf.leptos_options.site_addr; + // Generate the list of routes in your Leptos App + let routes = generate_route_list(App); + println!("listening on http://{}", &addr); + + HttpServer::new(move || { + let leptos_options = &conf.leptos_options; + let site_root = &leptos_options.site_root; + + App::new() + .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) + // serve JS/WASM/CSS from `pkg` + .service(Files::new("/pkg", format!("{site_root}/pkg"))) + .leptos_routes(leptos_options.to_owned(), routes.to_owned(), App) + .app_data(web::Data::new(leptos_options.to_owned())) + //.wrap(middleware::Compress::default()) + }) + .bind(&addr)? + .run() + .await +} + +#[cfg(not(any(feature = "ssr", feature = "csr")))] +pub fn main() { + // no client-side main function + // unless we want this to work with e.g., Trunk for pure client-side testing + // see lib.rs for hydration function instead + // see optional feature `csr` instead +} + +#[cfg(all(not(feature = "ssr"), feature = "csr"))] +pub fn main() { + // a client-side main function is required for using `trunk serve` + // prefer using `cargo leptos serve` instead + // to run: `trunk serve --open --features csr` + use leptos::*; + use action_form_error_handling::app::*; + use wasm_bindgen::prelude::wasm_bindgen; + + console_error_panic_hook::set_once(); + + leptos::mount_to_body(App); +} diff --git a/examples/action-form-error-handling/style/main.scss b/examples/action-form-error-handling/style/main.scss new file mode 100644 index 0000000000..908b7515df --- /dev/null +++ b/examples/action-form-error-handling/style/main.scss @@ -0,0 +1,15 @@ +body { + font-family: sans-serif; + text-align: center; +} + +#app { + text-align: center; +} + +.form { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} diff --git a/integrations/actix/Cargo.toml b/integrations/actix/Cargo.toml index aafd392755..7f09700fea 100644 --- a/integrations/actix/Cargo.toml +++ b/integrations/actix/Cargo.toml @@ -22,6 +22,7 @@ parking_lot = "0.12.1" regex = "1.7.0" tracing = "0.1.37" tokio = { version = "1", features = ["rt", "fs"] } +url = "2.5.0" [features] nonce = ["leptos/nonce"] diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index e49c85c50f..224d0e2a1d 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -6,7 +6,10 @@ //! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples) //! directory in the Leptos repository. -use actix_http::header::{HeaderName, HeaderValue, ACCEPT}; +use actix_http::{ + body::MessageBody, + header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER}, +}; use actix_web::{ body::BoxBody, dev::{ServiceFactory, ServiceRequest}, @@ -25,15 +28,21 @@ use leptos_meta::*; use leptos_router::*; use parking_lot::RwLock; use regex::Regex; -use server_fn::{redirect::REDIRECT_HEADER, request::actix::ActixRequest}; +use server_fn::{ + error::{NoCustomError, ServerFnErrorSerde, ServerFnUrlError}, + redirect::REDIRECT_HEADER, + request::actix::ActixRequest, +}; use std::{ fmt::{Debug, Display}, future::Future, pin::Pin, + str::FromStr, sync::Arc, }; #[cfg(debug_assertions)] use tracing::instrument; +use url::Url; /// This struct lets you define headers and override the status of the Response from an Element or a Server Function /// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses. #[derive(Debug, Clone, Default)] @@ -241,12 +250,80 @@ pub fn handle_server_fns_with_context( let res_parts = ResponseOptions::default(); provide_context(res_parts.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 = service .0 .run(ActixRequest::from((req, payload))) .await .take(); + // it it accepts text/html (i.e., is a plain form post) and doesn't already have a + // Location set, then redirect to to Referer + if accepts_html { + if let Some(mut referrer) = referrer { + let location = res.headers().get(LOCATION); + if location.is_none() { + let is_error = res.status() + == StatusCode::INTERNAL_SERVER_ERROR; + if is_error { + let (headers, body) = res.into_parts(); + if let Ok(body) = body.try_into_bytes() { + if let Ok(body) = + String::from_utf8(body.to_vec()) + { + // TODO allow other kinds? + if let Ok(err) = ServerFnUrlError::< + NoCustomError, + >::from_str( + &body + ) { + if let Ok(referrer_str) = + referrer.to_str() + { + let mut modified = + Url::parse(referrer_str) + .expect( + "couldn't parse \ + URL from Referer \ + header.", + ); + modified + .query_pairs_mut() + .append_pair( + "__path", + err.path(), + ) + .append_pair( + "__err", + &ServerFnErrorSerde::ser(err.error()) + .unwrap_or_else(|_| err.error().to_string()) + ); + let modified = + HeaderValue::from_str( + modified.as_ref(), + ); + if let Ok(header) = modified { + referrer = header; + } + } + } + } + } + res = headers.set_body(BoxBody::new("")) + }; + *res.status_mut() = StatusCode::FOUND; + res.headers_mut().insert(LOCATION, referrer); + } + } + }; + // Override StatusCode if it was set in a Resource or Element if let Some(status) = res_parts.0.read().status { *res.status_mut() = status; diff --git a/leptos_server/src/action.rs b/leptos_server/src/action.rs index ef2eeae89c..15dc1e65e8 100644 --- a/leptos_server/src/action.rs +++ b/leptos_server/src/action.rs @@ -1,9 +1,12 @@ //use crate::{ServerFn, ServerFnError}; use leptos_reactive::{ batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*, - spawn_local, store_value, ReadSignal, RwSignal, StoredValue, + spawn_local, store_value, use_context, ReadSignal, RwSignal, StoredValue, +}; +use server_fn::{ + error::{NoCustomError, ServerFnUrlError}, + ServerFn, ServerFnError, }; -use server_fn::{ServerFn, ServerFnError}; use std::{cell::Cell, future::Future, pin::Pin, rc::Rc}; /// An action synchronizes an imperative `async` call to the synchronous reactive system. @@ -168,6 +171,67 @@ where self.0.with_value(|a| a.pending.read_only()) } + /// Updates whether the action is currently pending. If the action has been dispatched + /// multiple times, and some of them are still pending, it will *not* update the `pending` + /// signal. + #[cfg_attr( + any(debug_assertions, feature = "ssr"), + tracing::instrument(level = "trace", skip_all,) + )] + pub fn set_pending(&self, pending: bool) { + self.0.try_with_value(|a| { + let pending_dispatches = &a.pending_dispatches; + let still_pending = { + pending_dispatches.set(if pending { + pending_dispatches.get().wrapping_add(1) + } else { + pending_dispatches.get().saturating_sub(1) + }); + pending_dispatches.get() + }; + if still_pending == 0 { + a.pending.set(false); + } else { + a.pending.set(true); + } + }); + } + + /// The URL associated with the action (typically as part of a server function.) + /// This enables integration with the `ActionForm` component in `leptos_router`. + pub fn url(&self) -> Option<String> { + self.0.with_value(|a| a.url.as_ref().cloned()) + } + + /// How many times the action has successfully resolved. + pub fn version(&self) -> RwSignal<usize> { + self.0.with_value(|a| a.version) + } + + /// The current argument that was dispatched to the `async` function. + /// `Some` while we are waiting for it to resolve, `None` if it has resolved. + #[cfg_attr( + any(debug_assertions, feature = "ssr"), + tracing::instrument(level = "trace", skip_all,) + )] + pub fn input(&self) -> RwSignal<Option<I>> { + self.0.with_value(|a| a.input) + } + + /// The most recent return value of the `async` function. + #[cfg_attr( + any(debug_assertions, feature = "ssr"), + tracing::instrument(level = "trace", skip_all,) + )] + pub fn value(&self) -> RwSignal<Option<O>> { + self.0.with_value(|a| a.value) + } +} + +impl<I> Action<I, Result<I::Output, ServerFnError<I::Error>>> +where + I: ServerFn + 'static, +{ /// Create an [Action] to imperatively call a [server_fn::server] function. /// /// The struct representing your server function's arguments should be @@ -214,7 +278,8 @@ where )] pub fn server() -> Action<I, Result<I::Output, ServerFnError<I::Error>>> where - I: ServerFn<Output = O> + Clone, + I: ServerFn + Clone, + I::Error: Clone + 'static, { // The server is able to call the function directly #[cfg(feature = "ssr")] @@ -225,39 +290,7 @@ where let action_function = |args: &I| I::run_on_client(args.clone()); // create the action - Action::new(action_function).using_server_fn::<I>() - } - - /// Updates whether the action is currently pending. If the action has been dispatched - /// multiple times, and some of them are still pending, it will *not* update the `pending` - /// signal. - #[cfg_attr( - any(debug_assertions, feature = "ssr"), - tracing::instrument(level = "trace", skip_all,) - )] - pub fn set_pending(&self, pending: bool) { - self.0.try_with_value(|a| { - let pending_dispatches = &a.pending_dispatches; - let still_pending = { - pending_dispatches.set(if pending { - pending_dispatches.get().wrapping_add(1) - } else { - pending_dispatches.get().saturating_sub(1) - }); - pending_dispatches.get() - }; - if still_pending == 0 { - a.pending.set(false); - } else { - a.pending.set(true); - } - }); - } - - /// The URL associated with the action (typically as part of a server function.) - /// This enables integration with the `ActionForm` component in `leptos_router`. - pub fn url(&self) -> Option<String> { - self.0.with_value(|a| a.url.as_ref().cloned()) + Action::new(action_function).using_server_fn() } /// Associates the URL of the given server function with this action. @@ -266,36 +299,27 @@ where any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) )] - pub fn using_server_fn<T: ServerFn>(self) -> Self { + pub fn using_server_fn(self) -> Self + where + I::Error: Clone + 'static, + { + let url = I::url(); + let action_error = use_context::<Rc<ServerFnUrlError<I::Error>>>() + .and_then(|err| { + if err.path() == url { + Some(err.error().clone()) + } else { + None + } + }); self.0.update_value(|state| { - state.url = Some(T::url().to_string()); + if let Some(err) = action_error { + state.value.set_untracked(Some(Err(err))); + } + state.url = Some(url.to_string()); }); self } - - /// How many times the action has successfully resolved. - pub fn version(&self) -> RwSignal<usize> { - self.0.with_value(|a| a.version) - } - - /// The current argument that was dispatched to the `async` function. - /// `Some` while we are waiting for it to resolve, `None` if it has resolved. - #[cfg_attr( - any(debug_assertions, feature = "ssr"), - tracing::instrument(level = "trace", skip_all,) - )] - pub fn input(&self) -> RwSignal<Option<I>> { - self.0.with_value(|a| a.input) - } - - /// The most recent return value of the `async` function. - #[cfg_attr( - any(debug_assertions, feature = "ssr"), - tracing::instrument(level = "trace", skip_all,) - )] - pub fn value(&self) -> RwSignal<Option<O>> { - self.0.with_value(|a| a.value) - } } impl<I, O> Clone for Action<I, O> @@ -482,6 +506,7 @@ pub fn create_server_action<S>( ) -> Action<S, Result<S::Output, ServerFnError<S::Error>>> where S: Clone + ServerFn, + S::Error: Clone + 'static, { Action::<S, _>::server() } diff --git a/router/src/components/router.rs b/router/src/components/router.rs index 875c75d623..2c63fee8f4 100644 --- a/router/src/components/router.rs +++ b/router/src/components/router.rs @@ -1,12 +1,19 @@ use crate::{ - create_location, matching::resolve_path, scroll_to_el, use_navigate, - Branch, History, Location, LocationChange, RouteContext, + create_location, matching::resolve_path, scroll_to_el, use_location, + use_navigate, Branch, History, Location, LocationChange, RouteContext, RouterIntegrationContext, State, }; #[cfg(not(feature = "ssr"))] use crate::{unescape, Url}; use cfg_if::cfg_if; -use leptos::{logging::debug_warn, server_fn::redirect::RedirectHook, *}; +use leptos::{ + logging::debug_warn, + server_fn::{ + error::{ServerFnErrorSerde, ServerFnUrlError}, + redirect::RedirectHook, + }, + *, +}; #[cfg(feature = "transition")] use leptos_reactive::use_transition; use send_wrapper::SendWrapper; @@ -64,6 +71,16 @@ pub fn Router( debug_warn!("Error setting <Router/> server function redirect hook."); } + // provide ServerFnUrlError if it exists + let location = use_location(); + if let (Some(path), Some(err)) = location + .query + .with_untracked(|q| (q.get("__path").cloned(), q.get("__err").cloned())) + { + let err: ServerFnError = ServerFnErrorSerde::de(&err); + provide_context(Rc::new(ServerFnUrlError::new(path, err))) + } + children() } diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index 4771bd053d..e6c1f84dbe 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -399,3 +399,73 @@ impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> { } } } + +/// TODO: Write Documentation +#[derive(Debug)] +pub struct ServerFnUrlError<CustErr> { + path: String, + error: ServerFnError<CustErr>, +} + +impl<CustErr> FromStr for ServerFnUrlError<CustErr> +where + CustErr: FromStr + Display, +{ + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.split_once('|') { + None => Err(()), + Some((path, error)) => { + let error = ServerFnError::<CustErr>::de(error); + Ok(ServerFnUrlError { + path: path.to_string(), + error, + }) + } + } + } +} + +impl<CustErr> Display for ServerFnUrlError<CustErr> +where + CustErr: FromStr + Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}|", self.path)?; + write!(f, "{}", &self.error.ser()?)?; + Ok(()) + } +} + +impl<CustErr> ServerFnUrlError<CustErr> { + /// TODO: Write Documentation + pub fn new(path: impl Display, error: ServerFnError<CustErr>) -> Self { + Self { + path: path.to_string(), + error, + } + } + + /// TODO: Write documentation + pub fn error(&self) -> &ServerFnError<CustErr> { + &self.error + } + + /// TODO: Add docs + pub fn path(&self) -> &str { + &self.path + } +} + +impl<CustErr> From<ServerFnUrlError<CustErr>> for ServerFnError<CustErr> { + fn from(error: ServerFnUrlError<CustErr>) -> Self { + error.error + } +} + +impl<CustErr> From<ServerFnUrlError<CustErr>> for ServerFnErrorErr<CustErr> { + fn from(error: ServerFnUrlError<CustErr>) -> Self { + error.error.into() + } +} diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 23221ae972..279a722d31 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -237,9 +237,9 @@ where req: Self::ServerRequest, ) -> impl Future<Output = Self::ServerResponse> + Send { async { - Self::execute_on_server(req) - .await - .unwrap_or_else(Self::ServerResponse::error_response) + Self::execute_on_server(req).await.unwrap_or_else(|e| { + Self::ServerResponse::error_response(Self::PATH, e) + }) } } @@ -289,6 +289,7 @@ where } } + /// Runs the server function (on the server), bubbling up an `Err(_)` after any stage. #[doc(hidden)] fn execute_on_server( req: Self::ServerRequest, diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 4d093b53e5..ed537238cb 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -48,11 +48,12 @@ mod axum { &mut self, req: Request<Body>, ) -> Pin<Box<dyn Future<Output = Response<Body>> + Send>> { + let path = req.uri().path().to_string(); let inner = self.call(req); Box::pin(async move { inner.await.unwrap_or_else(|e| { let err = ServerFnError::from(e); - Response::<Body>::error_response(err) + Response::<Body>::error_response(&path, err) }) }) } @@ -125,11 +126,12 @@ mod actix { &mut self, req: HttpRequest, ) -> Pin<Box<dyn Future<Output = HttpResponse> + Send>> { + let path = req.uri().path().to_string(); let inner = self.call(req); Box::pin(async move { inner.await.unwrap_or_else(|e| { - let err = ServerFnError::from(e); - ActixResponse::error_response(err).take() + let err = ServerFnError::new(e); + ActixResponse::error_response(&path, err).take() }) }) } @@ -145,11 +147,12 @@ mod actix { &mut self, req: ActixRequest, ) -> Pin<Box<dyn Future<Output = ActixResponse> + Send>> { + let path = req.0 .0.uri().path().to_string(); let inner = self.call(req.0.take().0); Box::pin(async move { ActixResponse::from(inner.await.unwrap_or_else(|e| { - let err = ServerFnError::from(e); - ActixResponse::error_response(err).take() + let err = ServerFnError::new(e); + ActixResponse::error_response(&path, err).take() })) }) } diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index a0c6efa6bf..14b630ea70 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -1,5 +1,5 @@ use super::Res; -use crate::error::{ServerFnError, ServerFnErrorErr}; +use crate::error::{ServerFnError, ServerFnErrorErr, ServerFnUrlError}; use actix_web::{ http::{header, StatusCode}, HttpResponse, @@ -7,7 +7,10 @@ use actix_web::{ use bytes::Bytes; use futures::{Stream, StreamExt}; use send_wrapper::SendWrapper; -use std::fmt::{Debug, Display}; +use std::{ + fmt::{Debug, Display}, + str::FromStr, +}; /// A wrapped Actix response. /// @@ -31,7 +34,7 @@ impl From<HttpResponse> for ActixResponse { impl<CustErr> Res<CustErr> for ActixResponse where - CustErr: Display + Debug + 'static, + CustErr: FromStr + Display + Debug + 'static, { fn try_from_string( content_type: &str, @@ -57,13 +60,6 @@ where ))) } - fn error_response(err: ServerFnError<CustErr>) -> Self { - ActixResponse(SendWrapper::new( - HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR) - .body(err.to_string()), - )) - } - fn try_from_stream( content_type: &str, data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + 'static, @@ -77,4 +73,11 @@ where ), ))) } + + fn error_response(path: &str, err: ServerFnError<CustErr>) -> Self { + ActixResponse(SendWrapper::new( + HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR) + .body(ServerFnUrlError::new(path, err).to_string()), + )) + } } diff --git a/server_fn/src/response/http.rs b/server_fn/src/response/http.rs index 09844e72b4..ed6176bcd8 100644 --- a/server_fn/src/response/http.rs +++ b/server_fn/src/response/http.rs @@ -50,7 +50,7 @@ where .map_err(|e| ServerFnError::Response(e.to_string())) } - fn error_response(err: ServerFnError<CustErr>) -> Self { + fn error_response(path: &str, err: ServerFnError<CustErr>) -> Self { Response::builder() .status(http::StatusCode::INTERNAL_SERVER_ERROR) .body(Body::from(err.to_string())) diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index c6bf5a4a3a..620b5395fa 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -42,7 +42,7 @@ where ) -> Result<Self, ServerFnError<CustErr>>; /// Converts an error into a response, with a `500` status code and the error text as its body. - fn error_response(err: ServerFnError<CustErr>) -> Self; + fn error_response(path: &str, err: ServerFnError<CustErr>) -> Self; } /// Represents the response as received by the client. @@ -101,7 +101,7 @@ impl<CustErr> Res<CustErr> for BrowserMockRes { unreachable!() } - fn error_response(_err: ServerFnError<CustErr>) -> Self { + fn error_response(_path: &str, _err: ServerFnError<CustErr>) -> Self { unreachable!() }