From afdc02f31c062466cbc5ac202ff67e67d0fdac9d Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 Date: Sat, 28 Oct 2023 12:23:47 -0400 Subject: [PATCH 01/22] Make ActionForm example to test with --- examples/Makefile.toml | 1 + .../action-form-error-handling/Cargo.toml | 94 ++++++++++++++++++ .../action-form-error-handling/Makefile.toml | 8 ++ examples/action-form-error-handling/README.md | 68 +++++++++++++ .../assets/favicon.ico | Bin 0 -> 15406 bytes .../rust-toolchain.toml | 3 + .../action-form-error-handling/src/app.rs | 63 ++++++++++++ .../action-form-error-handling/src/lib.rs | 19 ++++ .../action-form-error-handling/src/main.rs | 69 +++++++++++++ .../style/main.scss | 4 + 10 files changed, 329 insertions(+) create mode 100644 examples/action-form-error-handling/Cargo.toml create mode 100644 examples/action-form-error-handling/Makefile.toml create mode 100644 examples/action-form-error-handling/README.md create mode 100644 examples/action-form-error-handling/assets/favicon.ico create mode 100644 examples/action-form-error-handling/rust-toolchain.toml create mode 100644 examples/action-form-error-handling/src/app.rs create mode 100644 examples/action-form-error-handling/src/lib.rs create mode 100644 examples/action-form-error-handling/src/main.rs create mode 100644 examples/action-form-error-handling/style/main.scss diff --git a/examples/Makefile.toml b/examples/Makefile.toml index 4373546f68..1612cdb5df 100644 --- a/examples/Makefile.toml +++ b/examples/Makefile.toml @@ -5,6 +5,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CARGO_BUILD_TEST_FLAGS = "" CARGO_MAKE_WORKSPACE_EMULATION = true CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [ + "action_form_error_handling", "animated_show", "counter", "counter_isomorphic", diff --git a/examples/action-form-error-handling/Cargo.toml b/examples/action-form-error-handling/Cargo.toml new file mode 100644 index 0000000000..937e36b171 --- /dev/null +++ b/examples/action-form-error-handling/Cargo.toml @@ -0,0 +1,94 @@ +[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 = { version = "0.5", features = ["nightly"] } +leptos_meta = { version = "0.5", features = ["nightly"] } +leptos_actix = { version = "0.5", optional = true } +leptos_router = { version = "0.5", features = ["nightly"] } +wasm-bindgen = "=0.2.87" + +[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" +# Assets source dir. All files found here will be copied and synchronized to site-root. +# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir. +# +# Optional. Env: LEPTOS_ASSETS_DIR. +assets-dir = "assets" +# 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/assets/favicon.ico b/examples/action-form-error-handling/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2ba8527cb12f5f28f331b8d361eef560492d4c77 GIT binary patch literal 15406 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 k<4FtN!5 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> + <Routes> + <Route path="" view=HomePage/> + <Route path="/*any" view=NotFound/> + </Routes> + </main> + </Router> + } +} + +/// Renders the home page of your application. +#[component] +fn HomePage() -> impl IntoView { + // Creates a reactive value to update the button + let (count, set_count) = create_signal(0); + let on_click = move |_| set_count.update(|count| *count += 1); + + view! { + <h1>"Welcome to Leptos!"</h1> + <button on:click=on_click>"Click Me: " {count}</button> + } +} + +/// 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..41a6393d9e --- /dev/null +++ b/examples/action-form-error-handling/src/lib.rs @@ -0,0 +1,19 @@ +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::*; + use leptos::*; + + 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..6128ec8895 --- /dev/null +++ b/examples/action-form-error-handling/src/main.rs @@ -0,0 +1,69 @@ +#[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"))) + // serve other assets from the `assets` directory + .service(Files::new("/assets", site_root)) + // serve the favicon from /favicon.ico + .service(favicon) + .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(feature = "ssr")] +#[actix_web::get("favicon.ico")] +async fn favicon( + leptos_options: actix_web::web::Data<leptos::LeptosOptions>, +) -> actix_web::Result<actix_files::NamedFile> { + let leptos_options = leptos_options.into_inner(); + let site_root = &leptos_options.site_root; + Ok(actix_files::NamedFile::open(format!( + "{site_root}/favicon.ico" + ))?) +} + +#[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..e4538e156f --- /dev/null +++ b/examples/action-form-error-handling/style/main.scss @@ -0,0 +1,4 @@ +body { + font-family: sans-serif; + text-align: center; +} \ No newline at end of file From e32787906499eb7c4479bbf9b66777f3d4fd1bfa Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <abias1122@gmail.com> Date: Sun, 29 Oct 2023 12:18:30 -0400 Subject: [PATCH 02/22] Setup example --- .../action-form-error-handling/Cargo.toml | 6 +---- .../assets/favicon.ico | Bin 15406 -> 0 bytes .../action-form-error-handling/src/app.rs | 22 ++++++++++++++---- .../action-form-error-handling/src/main.rs | 16 ------------- .../style/main.scss | 9 ++++++- 5 files changed, 26 insertions(+), 27 deletions(-) delete mode 100644 examples/action-form-error-handling/assets/favicon.ico diff --git a/examples/action-form-error-handling/Cargo.toml b/examples/action-form-error-handling/Cargo.toml index 937e36b171..7b7e0947d3 100644 --- a/examples/action-form-error-handling/Cargo.toml +++ b/examples/action-form-error-handling/Cargo.toml @@ -17,6 +17,7 @@ leptos_meta = { version = "0.5", features = ["nightly"] } leptos_actix = { version = "0.5", optional = true } leptos_router = { version = "0.5", features = ["nightly"] } wasm-bindgen = "=0.2.87" +serde = { version = "1", features = ["derive"] } [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] @@ -48,11 +49,6 @@ site-root = "target/site" 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 <site-root>/<site-pkg>/app.css style-file = "style/main.scss" -# Assets source dir. All files found here will be copied and synchronized to site-root. -# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir. -# -# Optional. Env: LEPTOS_ASSETS_DIR. -assets-dir = "assets" # 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 diff --git a/examples/action-form-error-handling/assets/favicon.ico b/examples/action-form-error-handling/assets/favicon.ico deleted file mode 100644 index 2ba8527cb12f5f28f331b8d361eef560492d4c77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 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 diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs index 8e8a378b3f..bf4ac2d22f 100644 --- a/examples/action-form-error-handling/src/app.rs +++ b/examples/action-form-error-handling/src/app.rs @@ -27,16 +27,28 @@ pub fn App() -> impl IntoView { } } +#[server] +async fn do_something(should_error: Option<String>) -> Result<String, ServerFnError> { + if should_error.is_some() { + 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 { - // Creates a reactive value to update the button - let (count, set_count) = create_signal(0); - let on_click = move |_| set_count.update(|count| *count += 1); + let do_something_action = create_server_action::<DoSomething>(); view! { - <h1>"Welcome to Leptos!"</h1> - <button on:click=on_click>"Click Me: " {count}</button> + <h1>"Test the action form!"</h1> + <ActionForm action=do_something_action class="form"> + <label>Should error: <input type="checkbox" name="should_error"/></label> + <button type="submit">Submit</button> + </ActionForm> } } diff --git a/examples/action-form-error-handling/src/main.rs b/examples/action-form-error-handling/src/main.rs index 6128ec8895..d9cbb052e8 100644 --- a/examples/action-form-error-handling/src/main.rs +++ b/examples/action-form-error-handling/src/main.rs @@ -21,10 +21,6 @@ async fn main() -> std::io::Result<()> { .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) // serve JS/WASM/CSS from `pkg` .service(Files::new("/pkg", format!("{site_root}/pkg"))) - // serve other assets from the `assets` directory - .service(Files::new("/assets", site_root)) - // serve the favicon from /favicon.ico - .service(favicon) .leptos_routes(leptos_options.to_owned(), routes.to_owned(), App) .app_data(web::Data::new(leptos_options.to_owned())) //.wrap(middleware::Compress::default()) @@ -34,18 +30,6 @@ async fn main() -> std::io::Result<()> { .await } -#[cfg(feature = "ssr")] -#[actix_web::get("favicon.ico")] -async fn favicon( - leptos_options: actix_web::web::Data<leptos::LeptosOptions>, -) -> actix_web::Result<actix_files::NamedFile> { - let leptos_options = leptos_options.into_inner(); - let site_root = &leptos_options.site_root; - Ok(actix_files::NamedFile::open(format!( - "{site_root}/favicon.ico" - ))?) -} - #[cfg(not(any(feature = "ssr", feature = "csr")))] pub fn main() { // no client-side main function diff --git a/examples/action-form-error-handling/style/main.scss b/examples/action-form-error-handling/style/main.scss index e4538e156f..152de65c4f 100644 --- a/examples/action-form-error-handling/style/main.scss +++ b/examples/action-form-error-handling/style/main.scss @@ -1,4 +1,11 @@ body { font-family: sans-serif; text-align: center; -} \ No newline at end of file +} + +.form { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} From a209b0ecd63b6b41173f8337bb5e52ceda16978f Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <abias1122@gmail.com> Date: Sun, 29 Oct 2023 17:59:45 -0400 Subject: [PATCH 03/22] Tweak error in example --- examples/action-form-error-handling/src/app.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs index bf4ac2d22f..c219923ab6 100644 --- a/examples/action-form-error-handling/src/app.rs +++ b/examples/action-form-error-handling/src/app.rs @@ -29,7 +29,7 @@ pub fn App() -> impl IntoView { #[server] async fn do_something(should_error: Option<String>) -> Result<String, ServerFnError> { - if should_error.is_some() { + if should_error.is_none() { Ok(String::from("Successful submit")) } else { Err(ServerFnError::ServerError(String::from( @@ -45,6 +45,9 @@ fn HomePage() -> impl IntoView { view! { <h1>"Test the action form!"</h1> + <ErrorBoundary fallback=move |error| format!("{:#?}", error().into_iter().next().unwrap().1.into_inner().to_string())> + {do_something_action.value()} + </ErrorBoundary> <ActionForm action=do_something_action class="form"> <label>Should error: <input type="checkbox" name="should_error"/></label> <button type="submit">Submit</button> From 3f282236272f0764c92a8082b3b8ef5bc954c21d Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <abias1122@gmail.com> Date: Tue, 31 Oct 2023 14:36:34 -0400 Subject: [PATCH 04/22] Use local leptos as action form example dependency --- examples/action-form-error-handling/Cargo.toml | 8 ++++---- examples/action-form-error-handling/rust-toolchain.toml | 3 --- examples/action-form-error-handling/src/app.rs | 4 ++-- examples/action-form-error-handling/src/lib.rs | 1 - examples/action-form-error-handling/style/main.scss | 4 ++++ 5 files changed, 10 insertions(+), 10 deletions(-) delete mode 100644 examples/action-form-error-handling/rust-toolchain.toml diff --git a/examples/action-form-error-handling/Cargo.toml b/examples/action-form-error-handling/Cargo.toml index 7b7e0947d3..b0e9f2e020 100644 --- a/examples/action-form-error-handling/Cargo.toml +++ b/examples/action-form-error-handling/Cargo.toml @@ -12,10 +12,10 @@ actix-web = { version = "4", optional = true, features = ["macros"] } console_error_panic_hook = "0.1" cfg-if = "1" http = { version = "0.2", optional = true } -leptos = { version = "0.5", features = ["nightly"] } -leptos_meta = { version = "0.5", features = ["nightly"] } -leptos_actix = { version = "0.5", optional = true } -leptos_router = { version = "0.5", features = ["nightly"] } +leptos = { path = "../../leptos" } +leptos_meta = { path = "../../meta" } +leptos_actix = { path = "../../integrations/actix", optional = true } +leptos_router = { path = "../../router" } wasm-bindgen = "=0.2.87" serde = { version = "1", features = ["derive"] } diff --git a/examples/action-form-error-handling/rust-toolchain.toml b/examples/action-form-error-handling/rust-toolchain.toml deleted file mode 100644 index e9743fb495..0000000000 --- a/examples/action-form-error-handling/rust-toolchain.toml +++ /dev/null @@ -1,3 +0,0 @@ - -[toolchain] -channel = "nightly" diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs index c219923ab6..8057b9c30f 100644 --- a/examples/action-form-error-handling/src/app.rs +++ b/examples/action-form-error-handling/src/app.rs @@ -17,7 +17,7 @@ pub fn App() -> impl IntoView { // content for this welcome page <Router> - <main> + <main id="app"> <Routes> <Route path="" view=HomePage/> <Route path="/*any" view=NotFound/> @@ -45,7 +45,7 @@ fn HomePage() -> impl IntoView { view! { <h1>"Test the action form!"</h1> - <ErrorBoundary fallback=move |error| format!("{:#?}", error().into_iter().next().unwrap().1.into_inner().to_string())> + <ErrorBoundary fallback=move |error| format!("{:#?}", error.get().into_iter().next().unwrap().1.into_inner().to_string())> {do_something_action.value()} </ErrorBoundary> <ActionForm action=do_something_action class="form"> diff --git a/examples/action-form-error-handling/src/lib.rs b/examples/action-form-error-handling/src/lib.rs index 41a6393d9e..f1b7478302 100644 --- a/examples/action-form-error-handling/src/lib.rs +++ b/examples/action-form-error-handling/src/lib.rs @@ -9,7 +9,6 @@ if #[cfg(feature = "hydrate")] { #[wasm_bindgen] pub fn hydrate() { use app::*; - use leptos::*; console_error_panic_hook::set_once(); diff --git a/examples/action-form-error-handling/style/main.scss b/examples/action-form-error-handling/style/main.scss index 152de65c4f..908b7515df 100644 --- a/examples/action-form-error-handling/style/main.scss +++ b/examples/action-form-error-handling/style/main.scss @@ -3,6 +3,10 @@ body { text-align: center; } +#app { + text-align: center; +} + .form { display: flex; flex-direction: column; From 708781ff4d5102a407542e34005c835d9b0e4a97 Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <abias1122@gmail.com> Date: Thu, 2 Nov 2023 19:25:30 -0400 Subject: [PATCH 05/22] Make actix redirect with error in url --- integrations/actix/Cargo.toml | 2 + integrations/actix/src/lib.rs | 79 ++++++++++++++++++++++----------- integrations/utils/Cargo.toml | 2 + integrations/utils/src/lib.rs | 14 +++++- router/src/components/router.rs | 1 - server_fn/src/error.rs | 1 + server_fn/src/lib.rs | 29 ++++-------- 7 files changed, 79 insertions(+), 49 deletions(-) diff --git a/integrations/actix/Cargo.toml b/integrations/actix/Cargo.toml index 8c21885a11..457e21ab29 100644 --- a/integrations/actix/Cargo.toml +++ b/integrations/actix/Cargo.toml @@ -16,10 +16,12 @@ leptos_meta = { workspace = true, features = ["ssr"] } leptos_router = { workspace = true, features = ["ssr"] } leptos_integration_utils = { workspace = true } serde_json = "1" +serde_qs = "0.12" parking_lot = "0.12.1" regex = "1.7.0" tracing = "0.1.37" tokio = { version = "1", features = ["rt", "fs"] } +url = "2.4" [features] nonce = ["leptos/nonce"] diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 6b3c385dad..36943166da 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -6,11 +6,10 @@ //! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples) //! directory in the Leptos repository. -use actix_http::header::{HeaderName, HeaderValue}; +use actix_http::{header::{self, HeaderName, HeaderValue}, h1}; use actix_web::{ body::BoxBody, dev::{ServiceFactory, ServiceRequest}, - http::header, web::{Bytes, ServiceConfig}, *, }; @@ -22,7 +21,7 @@ use leptos::{ ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement, *, }; -use leptos_integration_utils::{build_async_response, html_parts_separated}; +use leptos_integration_utils::{build_async_response, html_parts_separated, ServerFnErrorQuery, ServerFnErrorInfo}; use leptos_meta::*; use leptos_router::*; use parking_lot::RwLock; @@ -33,6 +32,7 @@ use std::{ pin::Pin, sync::Arc, }; +use url::Url; #[cfg(debug_assertions)] use tracing::instrument; /// This struct lets you define headers and override the status of the Response from an Element or a Server Function @@ -182,7 +182,7 @@ pub fn handle_server_fns_with_context( let path = params.into_inner(); let accept_header = req .headers() - .get("Accept") + .get(header::ACCEPT) .and_then(|value| value.to_str().ok()); if let Some(server_fn) = server_fn_by_path(path.as_str()) { @@ -202,12 +202,12 @@ pub fn handle_server_fns_with_context( // like MultipartForm if req .headers() - .get("Content-Type") + .get(header::CONTENT_TYPE) .and_then(|value| value.to_str().ok()) .map(|value| { value.starts_with("multipart/form-data; boundary=") }) - == Some(true) + .unwrap_or(false) { provide_context(body.clone()); } @@ -229,7 +229,7 @@ pub fn handle_server_fns_with_context( 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 referrer if not redirect has been set + // 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") @@ -237,15 +237,15 @@ pub fn handle_server_fns_with_context( { // Location will already be set if redirect() has been used let has_location_set = - res_parts.headers.get("Location").is_some(); + res_parts.headers.get(header::LOCATION).is_some(); if !has_location_set { let referer = req .headers() - .get("Referer") + .get(header::REFERER) .and_then(|value| value.to_str().ok()) .unwrap_or("/"); res = HttpResponse::SeeOther(); - res.insert_header(("Location", referer)) + res.insert_header((header::LOCATION, referer)) .content_type("application/json"); } }; @@ -281,10 +281,37 @@ pub fn handle_server_fns_with_context( } } } - Err(e) => HttpResponse::InternalServerError().body( - serde_json::to_string(&e) - .unwrap_or_else(|_| e.to_string()), - ), + Err(e) => { + let url = req + .headers() + .get(header::REFERER) + .and_then(|value| + value + .to_str() + .ok() + .map(Url::parse) + .map(Result::ok) + ) + .flatten(); + + if let Some(mut url) = url { + url.query_pairs_mut() + .append_key_only(serde_qs::to_string(&ServerFnErrorQuery { + server_fn_error: ServerFnErrorInfo { + url: req.uri().to_string(), + error: e + } + }).expect("Could not serialize server fn error").as_str()); + HttpResponse::SeeOther() + .insert_header((header::LOCATION, url.to_string())) + .finish() + } else { + HttpResponse::InternalServerError().body( + serde_json::to_string(&e) + .unwrap_or_else(|_| e.to_string()), + ) + } + } }; // clean up the scope runtime.dispose(); @@ -1214,7 +1241,7 @@ where #[tracing::instrument(level = "trace", fields(error), skip_all)] fn leptos_routes_with_context<IV>( - self, + mut self, options: LeptosOptions, paths: Vec<RouteListing>, additional_context: impl Fn() + 'static + Clone + Send, @@ -1223,14 +1250,13 @@ where where IV: IntoView + 'static, { - let mut router = self; for listing in paths.iter() { let path = listing.path(); let mode = listing.mode(); for method in listing.methods() { - router = if let Some(static_mode) = listing.static_mode() { - router.route( + self = if let Some(static_mode) = listing.static_mode() { + self.route( path, static_route( options.clone(), @@ -1241,7 +1267,7 @@ where ), ) } else { - router.route( + self.route( path, match mode { SsrMode::OutOfOrder => { @@ -1280,7 +1306,8 @@ where }; } } - router + + self } } @@ -1302,7 +1329,7 @@ impl LeptosRoutes for &mut ServiceConfig { #[tracing::instrument(level = "trace", fields(error), skip_all)] fn leptos_routes_with_context<IV>( - self, + mut self, options: LeptosOptions, paths: Vec<RouteListing>, additional_context: impl Fn() + 'static + Clone + Send, @@ -1311,13 +1338,12 @@ impl LeptosRoutes for &mut ServiceConfig { where IV: IntoView + 'static, { - let mut router = self; for listing in paths.iter() { let path = listing.path(); let mode = listing.mode(); for method in listing.methods() { - router = router.route( + self = self.route( path, match mode { SsrMode::OutOfOrder => { @@ -1355,7 +1381,8 @@ impl LeptosRoutes for &mut ServiceConfig { ); } } - router + + self } } @@ -1403,7 +1430,7 @@ where .expect("HttpRequest should have been provided via context"); let input = if let Some(body) = use_context::<Bytes>() { - let (_, mut payload) = actix_http::h1::Payload::create(false); + let (_, mut payload) = h1::Payload::create(false); payload.unread_data(body); E::from_request(&req, &mut dev::Payload::from(payload)) } else { @@ -1442,7 +1469,7 @@ where .expect("HttpRequest should have been provided via context"); if let Some(body) = use_context::<Bytes>() { - let (_, mut payload) = actix_http::h1::Payload::create(false); + let (_, mut payload) = h1::Payload::create(false); payload.unread_data(body); T::from_request(&req, &mut dev::Payload::from(payload)) } else { diff --git a/integrations/utils/Cargo.toml b/integrations/utils/Cargo.toml index f655069a23..3a65a69fa1 100644 --- a/integrations/utils/Cargo.toml +++ b/integrations/utils/Cargo.toml @@ -14,6 +14,8 @@ leptos_hot_reload = { workspace = true } leptos_meta = { workspace = true, features = ["ssr"] } leptos_config = { workspace = true } tracing = "0.1.37" +serde_qs = "0.12" +serde = { version = "1", features = ["derive"] } [features] experimental-islands = [] diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs index 702e18fcc7..7895811fd4 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -1,10 +1,22 @@ use futures::{Stream, StreamExt}; -use leptos::{nonce::use_nonce, use_context, RuntimeId}; +use leptos::{nonce::use_nonce, use_context, RuntimeId, ServerFnError}; use leptos_config::LeptosOptions; use leptos_meta::MetaContext; +use serde::{Deserialize, Serialize}; extern crate tracing; +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ServerFnErrorInfo { + pub url: String, + pub error: ServerFnError +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ServerFnErrorQuery { + pub server_fn_error: ServerFnErrorInfo +} + #[tracing::instrument(level = "trace", fields(error), skip_all)] fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String { let reload_port = match options.reload_external_port { diff --git a/router/src/components/router.rs b/router/src/components/router.rs index 95e5d2a985..3b21b6d2fb 100644 --- a/router/src/components/router.rs +++ b/router/src/components/router.rs @@ -170,7 +170,6 @@ impl RouterContext { location, base, history: Box::new(history), - reference, set_reference, referrers, diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index 6f621c4aae..9fe58d8501 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -55,6 +55,7 @@ impl From<ServerFnError> for Error { /// This means that other error types can easily be converted into it using the /// `?` operator. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(tag = "type", content = "message")] pub enum ServerFnError { /// Error while trying to register the server function (only occurs in case of poisoned RwLock). Registration(String), diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 1d9a3a0e09..b88aeeeb1a 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -378,36 +378,23 @@ where .map_err(|e| ServerFnError::Deserialization(e.to_string())), }; Box::pin(async move { - let value: Self = match value { - Ok(v) => v, - Err(e) => return Err(e), - }; + let value: Self = value?; // call the function - let result = match value.call_fn(cx).await { - Ok(r) => r, - Err(e) => return Err(e), - }; + let result = value.call_fn(cx).await?; // serialize the output let result = match Self::encoding() { Encoding::Url | Encoding::GetJSON => { - match serde_json::to_string(&result).map_err(|e| { - ServerFnError::Serialization(e.to_string()) - }) { - Ok(r) => Payload::Url(r), - Err(e) => return Err(e), - } + let url = serde_json::to_string(&result) + .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + Payload::Url(url) } Encoding::Cbor | Encoding::GetCBOR => { let mut buffer: Vec<u8> = Vec::new(); - match ciborium::ser::into_writer(&result, &mut buffer) - .map_err(|e| { - ServerFnError::Serialization(e.to_string()) - }) { - Ok(_) => Payload::Binary(buffer), - Err(e) => return Err(e), - } + ciborium::ser::into_writer(&result, &mut buffer) + .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + Payload::Binary(buffer) } }; From 3fe47ce8738921f15e53521b5c210bfd93416889 Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Wed, 13 Dec 2023 22:21:36 -0500 Subject: [PATCH 06/22] Make ServerFn Serialization easier --- examples/action-form-error-handling/Cargo.toml | 2 +- integrations/actix/src/lib.rs | 18 ++++-------------- integrations/utils/src/lib.rs | 14 +------------- server_fn/src/error.rs | 2 +- 4 files changed, 7 insertions(+), 29 deletions(-) diff --git a/examples/action-form-error-handling/Cargo.toml b/examples/action-form-error-handling/Cargo.toml index b0e9f2e020..80500d4821 100644 --- a/examples/action-form-error-handling/Cargo.toml +++ b/examples/action-form-error-handling/Cargo.toml @@ -16,7 +16,7 @@ leptos = { path = "../../leptos" } leptos_meta = { path = "../../meta" } leptos_actix = { path = "../../integrations/actix", optional = true } leptos_router = { path = "../../router" } -wasm-bindgen = "=0.2.87" +wasm-bindgen = "0.2" serde = { version = "1", features = ["derive"] } [features] diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 98d010fe2c..f99a75c1d9 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -21,7 +21,7 @@ use leptos::{ ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement, *, }; -use leptos_integration_utils::{build_async_response, html_parts_separated, ServerFnErrorQuery, ServerFnErrorInfo}; +use leptos_integration_utils::{build_async_response, html_parts_separated}; use leptos_meta::*; use leptos_router::*; use parking_lot::RwLock; @@ -292,22 +292,12 @@ pub fn handle_server_fns_with_context( .headers() .get(header::REFERER) .and_then(|value| - value - .to_str() - .ok() - .map(Url::parse) - .map(Result::ok) - ) - .flatten(); + Url::parse(&Regex::new(r"(?:\?|&)?server_fn_error=[^&]+").unwrap().replace(value.to_str().ok()?, "")).ok() + ); if let Some(mut url) = url { url.query_pairs_mut() - .append_key_only(serde_qs::to_string(&ServerFnErrorQuery { - server_fn_error: ServerFnErrorInfo { - url: req.uri().to_string(), - error: e - } - }).expect("Could not serialize server fn error").as_str()); + .append_pair("server_fn_error", serde_qs::to_string(&e).expect("Could not serialize server fn error!").as_str()); HttpResponse::SeeOther() .insert_header((header::LOCATION, url.to_string())) .finish() diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs index 7895811fd4..702e18fcc7 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -1,22 +1,10 @@ use futures::{Stream, StreamExt}; -use leptos::{nonce::use_nonce, use_context, RuntimeId, ServerFnError}; +use leptos::{nonce::use_nonce, use_context, RuntimeId}; use leptos_config::LeptosOptions; use leptos_meta::MetaContext; -use serde::{Deserialize, Serialize}; extern crate tracing; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ServerFnErrorInfo { - pub url: String, - pub error: ServerFnError -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ServerFnErrorQuery { - pub server_fn_error: ServerFnErrorInfo -} - #[tracing::instrument(level = "trace", fields(error), skip_all)] fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String { let reload_port = match options.reload_external_port { diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index 489fc6d42f..d345df6b7d 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -54,7 +54,7 @@ impl From<ServerFnError> for Error { /// Unlike [`ServerFnErrorErr`], this does not implement [`std::error::Error`]. /// This means that other error types can easily be converted into it using the /// `?` operator. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(tag = "type", content = "message")] pub enum ServerFnError { /// Error while trying to register the server function (only occurs in case of poisoned RwLock). From ccd43cef9d12c76089ae769463e8b7ac39718ab3 Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Thu, 14 Dec 2023 23:22:46 -0500 Subject: [PATCH 07/22] Formatting --- integrations/actix/src/lib.rs | 49 ++++++++++++++++++++++++++--------- server_fn/src/lib.rs | 10 ++++--- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index f99a75c1d9..265763cb13 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::{self, HeaderName, HeaderValue}, h1}; +use actix_http::{ + h1, + header::{self, HeaderName, HeaderValue}, +}; use actix_web::{ body::BoxBody, dev::{ServiceFactory, ServiceRequest}, @@ -32,9 +35,9 @@ use std::{ pin::Pin, sync::Arc, }; -use url::Url; #[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)] @@ -242,8 +245,10 @@ pub fn handle_server_fns_with_context( && accept_header != Some("application/cbor") { // Location will already be set if redirect() has been used - let has_location_set = - res_parts.headers.get(header::LOCATION).is_some(); + let has_location_set = res_parts + .headers + .get(header::LOCATION) + .is_some(); if !has_location_set { let referer = req .headers() @@ -251,8 +256,11 @@ pub fn handle_server_fns_with_context( .and_then(|value| value.to_str().ok()) .unwrap_or("/"); res = HttpResponse::SeeOther(); - res.insert_header((header::LOCATION, referer)) - .content_type("application/json"); + res.insert_header(( + header::LOCATION, + referer, + )) + .content_type("application/json"); } }; // Override StatusCode if it was set in a Resource or Element @@ -291,15 +299,32 @@ pub fn handle_server_fns_with_context( let url = req .headers() .get(header::REFERER) - .and_then(|value| - Url::parse(&Regex::new(r"(?:\?|&)?server_fn_error=[^&]+").unwrap().replace(value.to_str().ok()?, "")).ok() - ); + .and_then(|value| { + Url::parse( + &Regex::new( + r"(?:\?|&)?server_fn_error=[^&]+", + ) + .unwrap() + .replace(value.to_str().ok()?, ""), + ) + .ok() + }); if let Some(mut url) = url { - url.query_pairs_mut() - .append_pair("server_fn_error", serde_qs::to_string(&e).expect("Could not serialize server fn error!").as_str()); + url.query_pairs_mut().append_pair( + "server_fn_error", + serde_qs::to_string(&e) + .expect( + "Could not serialize server fn \ + error!", + ) + .as_str(), + ); HttpResponse::SeeOther() - .insert_header((header::LOCATION, url.to_string())) + .insert_header(( + header::LOCATION, + url.to_string(), + )) .finish() } else { HttpResponse::InternalServerError().body( diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index b88aeeeb1a..e677f9c672 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -386,14 +386,16 @@ where // serialize the output let result = match Self::encoding() { Encoding::Url | Encoding::GetJSON => { - let url = serde_json::to_string(&result) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + let url = serde_json::to_string(&result).map_err(|e| { + ServerFnError::Serialization(e.to_string()) + })?; Payload::Url(url) } Encoding::Cbor | Encoding::GetCBOR => { let mut buffer: Vec<u8> = Vec::new(); - ciborium::ser::into_writer(&result, &mut buffer) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + ciborium::ser::into_writer(&result, &mut buffer).map_err( + |e| ServerFnError::Serialization(e.to_string()), + )?; Payload::Binary(buffer) } }; From 2cb58778195659803116b6d331540a25538e9f7d Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Fri, 22 Dec 2023 18:57:27 -0500 Subject: [PATCH 08/22] Implement serverfnerr encoding for axum integration and refactor --- integrations/actix/Cargo.toml | 2 -- integrations/actix/src/lib.rs | 35 +++++---------------- integrations/axum/Cargo.toml | 1 - integrations/axum/src/lib.rs | 57 +++++++++++++++++++++-------------- integrations/utils/Cargo.toml | 3 ++ integrations/utils/src/lib.rs | 31 ++++++++++++++++++- 6 files changed, 75 insertions(+), 54 deletions(-) diff --git a/integrations/actix/Cargo.toml b/integrations/actix/Cargo.toml index 457e21ab29..8c21885a11 100644 --- a/integrations/actix/Cargo.toml +++ b/integrations/actix/Cargo.toml @@ -16,12 +16,10 @@ leptos_meta = { workspace = true, features = ["ssr"] } leptos_router = { workspace = true, features = ["ssr"] } leptos_integration_utils = { workspace = true } serde_json = "1" -serde_qs = "0.12" parking_lot = "0.12.1" regex = "1.7.0" tracing = "0.1.37" tokio = { version = "1", features = ["rt", "fs"] } -url = "2.4" [features] nonce = ["leptos/nonce"] diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 1ac2376031..4ee3d9a3a5 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -8,7 +8,8 @@ use actix_http::{ h1, - header::{self, HeaderName, HeaderValue}, + header::{self, HeaderValue}, + StatusCode }; use actix_web::{ body::BoxBody, @@ -17,14 +18,13 @@ use actix_web::{ *, }; use futures::{Stream, StreamExt}; -use http::StatusCode; use leptos::{ leptos_server::{server_fn_by_path, Payload}, 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}; +use leptos_integration_utils::{build_async_response, html_parts_separated, referer_to_url, WithServerFn}; use leptos_meta::*; use leptos_router::*; use parking_lot::RwLock; @@ -37,7 +37,6 @@ use std::{ }; #[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)] @@ -299,31 +298,13 @@ pub fn handle_server_fns_with_context( let url = req .headers() .get(header::REFERER) - .and_then(|value| { - Url::parse( - &Regex::new( - r"(?:\?|&)?server_fn_error=[^&]+", - ) - .unwrap() - .replace(value.to_str().ok()?, ""), - ) - .ok() - }); - - if let Some(mut url) = url { - url.query_pairs_mut().append_pair( - "server_fn_error", - serde_qs::to_string(&e) - .expect( - "Could not serialize server fn \ - error!", - ) - .as_str(), - ); + .and_then(referer_to_url); + + if let Some(url) = url { HttpResponse::SeeOther() .insert_header(( header::LOCATION, - url.to_string(), + url.with_server_fn(&e).as_str(), )) .finish() } else { @@ -1089,7 +1070,7 @@ where }); if let Some(v) = content_type { res.headers_mut().insert( - HeaderName::from_static("content-type"), + header::CONTENT_TYPE, HeaderValue::from_static(v), ); } diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index f6091a8917..e0a36bca66 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -12,7 +12,6 @@ axum = { version = "0.6", default-features = false, features = [ "matched-path", ] } futures = "0.3" -http = "0.2.11" hyper = "0.14.23" leptos = { workspace = true, features = ["ssr"] } leptos_meta = { workspace = true, features = ["ssr"] } diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 24eede3ad3..342f124f38 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -37,8 +37,8 @@ use axum::{ body::{Body, Bytes, Full, StreamBody}, extract::{FromRef, FromRequestParts, MatchedPath, Path, RawQuery}, http::{ - header::{HeaderName, HeaderValue}, - HeaderMap, Request, StatusCode, + header::{self, HeaderName, HeaderValue}, request::Parts, uri::Uri, version::Version, Response, + HeaderMap, Request, StatusCode, method::Method }, response::IntoResponse, routing::{delete, get, patch, post, put}, @@ -47,10 +47,6 @@ use futures::{ channel::mpsc::{Receiver, Sender}, Future, SinkExt, Stream, StreamExt, }; -use http::{ - header, method::Method, request::Parts, uri::Uri, version::Version, - Response, -}; use hyper::body; use leptos::{ leptos_server::{server_fn_by_path, Payload}, @@ -58,7 +54,9 @@ use leptos::{ ssr::*, *, }; -use leptos_integration_utils::{build_async_response, html_parts_separated}; +use leptos_integration_utils::{ + build_async_response, html_parts_separated, referer_to_url, WithServerFn +}; use leptos_meta::{generate_head_metadata_separated, MetaContext}; use leptos_router::*; use once_cell::sync::OnceCell; @@ -337,13 +335,13 @@ async fn handle_server_fns_inner( // otherwise, it's probably a <form> submit or something: redirect back to the referrer else { let referer = headers - .get("Referer") + .get(header::REFERER) .and_then(|value| value.to_str().ok()) .unwrap_or("/"); res = res .status(StatusCode::SEE_OTHER) - .header("Location", referer); + .header(header::LOCATION, referer); } // Override StatusCode if it was set in a Resource or Element res = match status { @@ -357,25 +355,38 @@ async fn handle_server_fns_inner( }; match serialized { Payload::Binary(data) => res - .header("Content-Type", "application/cbor") + .header(header::CONTENT_TYPE, "application/cbor") .body(Full::from(data)), Payload::Url(data) => res .header( - "Content-Type", + header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) .body(Full::from(data)), Payload::Json(data) => res - .header("Content-Type", "application/json") + .header(header::CONTENT_TYPE, "application/json") .body(Full::from(data)), } } - Err(e) => Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Full::from( - serde_json::to_string(&e) - .unwrap_or_else(|_| e.to_string()), - )), + Err(e) => { + let referer = headers + .get(header::REFERER) + .and_then(referer_to_url); + + if let Some(referer) = referer { + Response::builder() + .status(StatusCode::SEE_OTHER) + .header(header::LOCATION, referer.with_server_fn(&e).as_str()) + .body(Default::default()) + } else { + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Full::from( + serde_json::to_string(&e) + .unwrap_or_else(|_| e.to_string()), + )) + } + } }; // clean up the scope runtime.dispose(); @@ -819,11 +830,11 @@ async fn generate_response( let mut res_headers = res_options.headers.clone(); headers.extend(res_headers.drain()); - if !headers.contains_key(http::header::CONTENT_TYPE) { + if !headers.contains_key(header::CONTENT_TYPE) { // Set the Content Type headers on all responses. This makes Firefox show the page source // without complaining headers.insert( - http::header::CONTENT_TYPE, + header::CONTENT_TYPE, HeaderValue::from_str("text/html; charset=utf-8").unwrap(), ); } @@ -1162,11 +1173,11 @@ where headers.extend(res_headers.drain()); // This one doesn't use generate_response(), so we need to do this seperately - if !headers.contains_key(http::header::CONTENT_TYPE) { + if !headers.contains_key(header::CONTENT_TYPE) { // Set the Content Type headers on all responses. This makes Firefox show the page source // without complaining headers.insert( - http::header::CONTENT_TYPE, + header::CONTENT_TYPE, HeaderValue::from_str("text/html; charset=utf-8") .unwrap(), ); @@ -1489,7 +1500,7 @@ where let mut res = Response::new(body); if let Some(v) = content_type { res.headers_mut().insert( - HeaderName::from_static("content-type"), + header::CONTENT_TYPE, HeaderValue::from_static(v), ); } diff --git a/integrations/utils/Cargo.toml b/integrations/utils/Cargo.toml index 3a65a69fa1..0cd16e04b9 100644 --- a/integrations/utils/Cargo.toml +++ b/integrations/utils/Cargo.toml @@ -16,6 +16,9 @@ leptos_config = { workspace = true } tracing = "0.1.37" serde_qs = "0.12" serde = { version = "1", features = ["derive"] } +url = "2.4" +http = "0.2.11" +regex = "1.7.0" [features] experimental-islands = [] diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs index 702e18fcc7..91050e2b75 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -1,7 +1,10 @@ use futures::{Stream, StreamExt}; -use leptos::{nonce::use_nonce, use_context, RuntimeId}; +use http::HeaderValue; +use leptos::{nonce::use_nonce, use_context, RuntimeId, ServerFnError}; use leptos_config::LeptosOptions; use leptos_meta::MetaContext; +use regex::Regex; +use url::Url; extern crate tracing; @@ -157,3 +160,29 @@ pub async fn build_async_response( format!("{head}<body{body_meta}>{buf}{tail}") } + +pub fn referer_to_url(referer: &HeaderValue) -> Option<Url> { + Url::parse( + &Regex::new(r"(?:\?|&)?server_fn_error=[^&]+") + .unwrap() + .replace(referer.to_str().ok()?, ""), + ) + .ok() +} + +pub trait WithServerFn { + fn with_server_fn(self, e: &ServerFnError) -> Self; +} + +impl WithServerFn for Url { + fn with_server_fn(mut self, e: &ServerFnError) -> Self { + self.query_pairs_mut().append_pair( + "server_fn_error", + serde_qs::to_string(e) + .expect("Could not serialize server fn error!") + .as_str(), + ); + + self + } +} From be3504e48c2c53b1d91572b61c15e0ce2fc1b71b Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Fri, 22 Dec 2023 19:25:51 -0500 Subject: [PATCH 09/22] Implement serverfn encoding for viz --- integrations/actix/src/lib.rs | 6 +++-- integrations/axum/src/lib.rs | 25 ++++++++++++++----- integrations/viz/Cargo.toml | 2 +- integrations/viz/src/lib.rs | 45 +++++++++++++++++++++++++---------- 4 files changed, 56 insertions(+), 22 deletions(-) diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 4ee3d9a3a5..632c82c607 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -9,7 +9,7 @@ use actix_http::{ h1, header::{self, HeaderValue}, - StatusCode + StatusCode, }; use actix_web::{ body::BoxBody, @@ -24,7 +24,9 @@ use leptos::{ ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement, *, }; -use leptos_integration_utils::{build_async_response, html_parts_separated, referer_to_url, WithServerFn}; +use leptos_integration_utils::{ + build_async_response, html_parts_separated, referer_to_url, WithServerFn, +}; use leptos_meta::*; use leptos_router::*; use parking_lot::RwLock; diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 342f124f38..1ab3e29145 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -37,8 +37,12 @@ use axum::{ body::{Body, Bytes, Full, StreamBody}, extract::{FromRef, FromRequestParts, MatchedPath, Path, RawQuery}, http::{ - header::{self, HeaderName, HeaderValue}, request::Parts, uri::Uri, version::Version, Response, - HeaderMap, Request, StatusCode, method::Method + header::{self, HeaderName, HeaderValue}, + method::Method, + request::Parts, + uri::Uri, + version::Version, + HeaderMap, Request, Response, StatusCode, }, response::IntoResponse, routing::{delete, get, patch, post, put}, @@ -55,7 +59,7 @@ use leptos::{ *, }; use leptos_integration_utils::{ - build_async_response, html_parts_separated, referer_to_url, WithServerFn + build_async_response, html_parts_separated, referer_to_url, WithServerFn, }; use leptos_meta::{generate_head_metadata_separated, MetaContext}; use leptos_router::*; @@ -355,7 +359,10 @@ async fn handle_server_fns_inner( }; match serialized { Payload::Binary(data) => res - .header(header::CONTENT_TYPE, "application/cbor") + .header( + header::CONTENT_TYPE, + "application/cbor", + ) .body(Full::from(data)), Payload::Url(data) => res .header( @@ -364,7 +371,10 @@ async fn handle_server_fns_inner( ) .body(Full::from(data)), Payload::Json(data) => res - .header(header::CONTENT_TYPE, "application/json") + .header( + header::CONTENT_TYPE, + "application/json", + ) .body(Full::from(data)), } } @@ -376,7 +386,10 @@ async fn handle_server_fns_inner( if let Some(referer) = referer { Response::builder() .status(StatusCode::SEE_OTHER) - .header(header::LOCATION, referer.with_server_fn(&e).as_str()) + .header( + header::LOCATION, + referer.with_server_fn(&e).as_str(), + ) .body(Default::default()) } else { Response::builder() diff --git a/integrations/viz/Cargo.toml b/integrations/viz/Cargo.toml index caead7be56..ea4613525a 100644 --- a/integrations/viz/Cargo.toml +++ b/integrations/viz/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/leptos-rs/leptos" description = "Viz integrations for the Leptos web framework." [dependencies] -viz = { version = "0.4.8" } +viz = "0.4.8" futures = "0.3" http = "0.2.11" hyper = "0.14.23" diff --git a/integrations/viz/src/lib.rs b/integrations/viz/src/lib.rs index f5648217cd..502e3504a4 100644 --- a/integrations/viz/src/lib.rs +++ b/integrations/viz/src/lib.rs @@ -10,7 +10,7 @@ use futures::{ channel::mpsc::{Receiver, Sender}, Future, SinkExt, Stream, StreamExt, }; -use http::{header, method::Method, uri::Uri, version::Version, StatusCode}; +use http::{uri::Uri, version::Version}; use hyper::body; use leptos::{ leptos_server::{server_fn_by_path, Payload}, @@ -18,16 +18,19 @@ use leptos::{ ssr::*, *, }; -use leptos_integration_utils::{build_async_response, html_parts_separated}; +use leptos_integration_utils::{ + build_async_response, html_parts_separated, referer_to_url, WithServerFn, +}; use leptos_meta::{generate_head_metadata_separated, MetaContext}; use leptos_router::*; use parking_lot::RwLock; use std::{pin::Pin, sync::Arc}; use tokio::task::spawn_blocking; use viz::{ + header, headers::{HeaderMap, HeaderName, HeaderValue}, - Body, Bytes, Error, Handler, IntoResponse, Request, RequestExt, Response, - ResponseExt, Result, Router, + Body, Bytes, Error, Handler, IntoResponse, Method, Request, RequestExt, + Response, ResponseExt, Result, Router, StatusCode, }; /// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced @@ -260,7 +263,7 @@ async fn handle_server_fns_inner( // otherwise, it's probably a <form> submit or something: redirect back to the referrer else { let referer = headers - .get("Referer") + .get(header::REFERER) .and_then(|value| { value.to_str().ok() }) @@ -268,7 +271,7 @@ async fn handle_server_fns_inner( res = res .status(StatusCode::SEE_OTHER) - .header("Location", referer); + .header(header::LOCATION, referer); } // Override StatusCode if it was set in a Resource or Element res = match status { @@ -297,12 +300,28 @@ async fn handle_server_fns_inner( .body(Body::from(data)), } } - Err(e) => Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from( - serde_json::to_string(&e) - .unwrap_or_else(|_| e.to_string()), - )), + Err(e) => { + let url = headers + .get(header::REFERER) + .and_then(referer_to_url); + + if let Some(url) = url { + Response::builder() + .status(StatusCode::SEE_OTHER) + .header( + header::LOCATION, + url.with_server_fn(&e).as_str(), + ) + .body(Default::default()) + } else { + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from( + serde_json::to_string(&e) + .unwrap_or_else(|_| e.to_string()), + )) + } + } }; runtime.dispose(); res @@ -1121,7 +1140,7 @@ where let mut res = Response::html(body); if let Some(v) = content_type { res.headers_mut().insert( - HeaderName::from_static("content-type"), + header::CONTENT_TYPE, HeaderValue::from_static(v), ); } From 554a5939d0715bfa52f8f21143ba2a3fabad2f68 Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Sat, 6 Jan 2024 16:26:40 -0500 Subject: [PATCH 10/22] Uniquely identify serverfn errors in URL --- integrations/actix/src/lib.rs | 2 +- integrations/axum/src/lib.rs | 4 ++-- integrations/utils/src/lib.rs | 6 +++--- integrations/viz/src/lib.rs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 632c82c607..e587ff2769 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -306,7 +306,7 @@ pub fn handle_server_fns_with_context( HttpResponse::SeeOther() .insert_header(( header::LOCATION, - url.with_server_fn(&e).as_str(), + url.with_server_fn(&e, path.as_str()).as_str(), )) .finish() } else { diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 1ab3e29145..0e66ac516d 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -285,7 +285,7 @@ async fn handle_server_fns_inner( // Axum Path extractor doesn't remove the first slash from the path, while Actix does let fn_name = fn_name .strip_prefix('/') - .map(|fn_name| fn_name.to_string()) + .map(ToString::to_string) .unwrap_or(fn_name); let (tx, rx) = futures::channel::oneshot::channel(); @@ -388,7 +388,7 @@ async fn handle_server_fns_inner( .status(StatusCode::SEE_OTHER) .header( header::LOCATION, - referer.with_server_fn(&e).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 91050e2b75..fd3e5cb7d6 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -171,13 +171,13 @@ pub fn referer_to_url(referer: &HeaderValue) -> Option<Url> { } pub trait WithServerFn { - fn with_server_fn(self, e: &ServerFnError) -> Self; + fn with_server_fn(self, e: &ServerFnError, path: &str) -> Self; } impl WithServerFn for Url { - fn with_server_fn(mut self, e: &ServerFnError) -> Self { + fn with_server_fn(mut self, e: &ServerFnError, path: &str) -> Self { self.query_pairs_mut().append_pair( - "server_fn_error", + format!("server_fn_error_{path}").as_str(), serde_qs::to_string(e) .expect("Could not serialize server fn error!") .as_str(), diff --git a/integrations/viz/src/lib.rs b/integrations/viz/src/lib.rs index 502e3504a4..dd1c416028 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(&e).as_str(), + url.with_server_fn(&e, fn_name.as_str()).as_str(), ) .body(Default::default()) } else { From 3794207d53af9795c38d12dd7d967afaf4863445 Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Sat, 6 Jan 2024 16:26:40 -0500 Subject: [PATCH 11/22] Uniquely identify serverfn errors in URL --- integrations/actix/src/lib.rs | 10 +++++----- integrations/axum/src/lib.rs | 8 ++++---- integrations/utils/src/lib.rs | 19 +++++++++++++------ integrations/viz/src/lib.rs | 6 +++--- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 632c82c607..a29e3434b2 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -25,7 +25,7 @@ use leptos::{ *, }; use leptos_integration_utils::{ - build_async_response, html_parts_separated, referer_to_url, WithServerFn, + build_async_response, html_parts_separated, referrer_to_url, WithServerFn, }; use leptos_meta::*; use leptos_router::*; @@ -189,13 +189,13 @@ pub fn handle_server_fns_with_context( async move { let additional_context = additional_context.clone(); - let path = params.into_inner(); + let fn_name = params.into_inner(); let accept_header = req .headers() .get(header::ACCEPT) .and_then(|value| value.to_str().ok()); - if let Some(server_fn) = server_fn_by_path(path.as_str()) { + if let Some(server_fn) = server_fn_by_path(fn_name.as_str()) { let body_ref: &[u8] = &body; let runtime = create_runtime(); @@ -300,13 +300,13 @@ pub fn handle_server_fns_with_context( let url = req .headers() .get(header::REFERER) - .and_then(referer_to_url); + .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(&e).as_str(), + url.with_server_fn(&e, fn_name.as_str()).as_str(), )) .finish() } else { diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 1ab3e29145..d442d3780a 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -59,7 +59,7 @@ use leptos::{ *, }; use leptos_integration_utils::{ - build_async_response, html_parts_separated, referer_to_url, WithServerFn, + build_async_response, html_parts_separated, referrer_to_url, WithServerFn, }; use leptos_meta::{generate_head_metadata_separated, MetaContext}; use leptos_router::*; @@ -285,7 +285,7 @@ async fn handle_server_fns_inner( // Axum Path extractor doesn't remove the first slash from the path, while Actix does let fn_name = fn_name .strip_prefix('/') - .map(|fn_name| fn_name.to_string()) + .map(ToString::to_string) .unwrap_or(fn_name); let (tx, rx) = futures::channel::oneshot::channel(); @@ -381,14 +381,14 @@ async fn handle_server_fns_inner( Err(e) => { let referer = headers .get(header::REFERER) - .and_then(referer_to_url); + .and_then(|referrer| referrer_to_url(referrer, fn_name.as_str())); if let Some(referer) = referer { Response::builder() .status(StatusCode::SEE_OTHER) .header( header::LOCATION, - referer.with_server_fn(&e).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 91050e2b75..f8f38adfee 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -4,6 +4,7 @@ use leptos::{nonce::use_nonce, use_context, RuntimeId, ServerFnError}; use leptos_config::LeptosOptions; use leptos_meta::MetaContext; use regex::Regex; +use serde::{Serialize, Deserialize}; use url::Url; extern crate tracing; @@ -161,9 +162,15 @@ pub async fn build_async_response( format!("{head}<body{body_meta}>{buf}{tail}") } -pub fn referer_to_url(referer: &HeaderValue) -> Option<Url> { +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ServerFnUrlError { + pub error: ServerFnError, + pub fn_name: String +} + +pub fn referrer_to_url(referer: &HeaderValue, fn_name: &str) -> Option<Url> { Url::parse( - &Regex::new(r"(?:\?|&)?server_fn_error=[^&]+") + &Regex::new(&format!(r"(?:\?|&)?server_fn_error_{fn_name}=[^&]+")) .unwrap() .replace(referer.to_str().ok()?, ""), ) @@ -171,14 +178,14 @@ pub fn referer_to_url(referer: &HeaderValue) -> Option<Url> { } pub trait WithServerFn { - fn with_server_fn(self, e: &ServerFnError) -> Self; + fn with_server_fn(self, error: &ServerFnError, fn_name: &str) -> Self; } impl WithServerFn for Url { - fn with_server_fn(mut self, e: &ServerFnError) -> Self { + fn with_server_fn(mut self, error: &ServerFnError, fn_name: &str) -> Self { self.query_pairs_mut().append_pair( - "server_fn_error", - serde_qs::to_string(e) + format!("server_fn_error_{fn_name}").as_str(), + serde_qs::to_string(&ServerFnUrlError{error: error.to_owned(), fn_name: fn_name.to_owned()}) .expect("Could not serialize server fn error!") .as_str(), ); diff --git a/integrations/viz/src/lib.rs b/integrations/viz/src/lib.rs index 502e3504a4..7a5c48a043 100644 --- a/integrations/viz/src/lib.rs +++ b/integrations/viz/src/lib.rs @@ -19,7 +19,7 @@ use leptos::{ *, }; use leptos_integration_utils::{ - build_async_response, html_parts_separated, referer_to_url, WithServerFn, + build_async_response, html_parts_separated, referrer_to_url, WithServerFn, }; use leptos_meta::{generate_head_metadata_separated, MetaContext}; use leptos_router::*; @@ -303,14 +303,14 @@ async fn handle_server_fns_inner( Err(e) => { let url = headers .get(header::REFERER) - .and_then(referer_to_url); + .and_then(|referrer| referrer_to_url(referrer, fn_name.as_str())); if let Some(url) = url { Response::builder() .status(StatusCode::SEE_OTHER) .header( header::LOCATION, - url.with_server_fn(&e).as_str(), + url.with_server_fn(&e, fn_name.as_str()).as_str(), ) .body(Default::default()) } else { From a3c8fe8c789ada2f9f592183de46a4ed92b940fd Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Mon, 8 Jan 2024 20:29:06 -0500 Subject: [PATCH 12/22] Make error gettable by server rendered action form --- integrations/actix/src/lib.rs | 22 ++++++++++++++++--- integrations/utils/src/lib.rs | 38 ++++++++++++++++++++++++-------- router/src/components/form.rs | 18 +++++++++++++-- server_fn/src/error.rs | 41 ++++++++++++++++++++++++++++++++++- 4 files changed, 104 insertions(+), 15 deletions(-) diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index a29e3434b2..9d7b422183 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -25,7 +25,8 @@ use leptos::{ *, }; use leptos_integration_utils::{ - build_async_response, html_parts_separated, referrer_to_url, WithServerFn, + build_async_response, filter_server_fn_url_errors, html_parts_separated, + referrer_to_url, WithServerFn, }; use leptos_meta::*; use leptos_router::*; @@ -300,13 +301,19 @@ pub fn handle_server_fns_with_context( let url = req .headers() .get(header::REFERER) - .and_then(|referrer| referrer_to_url(referrer, fn_name.as_str())); + .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(&e, fn_name.as_str()).as_str(), + url.with_server_fn( + &e, + fn_name.as_str(), + ) + .as_str(), )) .finish() } else { @@ -754,6 +761,15 @@ fn provide_contexts(req: &HttpRequest, res_options: ResponseOptions) { provide_context(MetaContext::new()); provide_context(res_options); provide_context(req.clone()); + if let Some(referrer) = req.headers().get(header::REFERER) { + leptos::logging::log!("Referrer = {referrer:?}"); + provide_context(filter_server_fn_url_errors( + referrer + .to_str() + .expect("Invalid referer header from browser request"), + )); + } + provide_server_redirect(redirect); #[cfg(feature = "nonce")] leptos::nonce::provide_nonce(); diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs index f8f38adfee..044df763e8 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -1,10 +1,13 @@ use futures::{Stream, StreamExt}; use http::HeaderValue; -use leptos::{nonce::use_nonce, use_context, RuntimeId, ServerFnError}; +use leptos::{ + nonce::use_nonce, server_fn::error::ServerFnUrlError, use_context, + RuntimeId, ServerFnError, +}; use leptos_config::LeptosOptions; use leptos_meta::MetaContext; use regex::Regex; -use serde::{Serialize, Deserialize}; +use std::collections::HashSet; use url::Url; extern crate tracing; @@ -162,10 +165,24 @@ pub async fn build_async_response( format!("{head}<body{body_meta}>{buf}{tail}") } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ServerFnUrlError { - pub error: ServerFnError, - pub fn_name: String +pub fn filter_server_fn_url_errors<'a>( + referrer: impl Into<&'a str>, +) -> HashSet<ServerFnUrlError> { + Url::parse(referrer.into()) + .expect("Cannot parse referrer from page request") + .query_pairs() + .into_iter() + .filter_map(|(k, v)| { + + let foo = if k.starts_with("server_fn_error_") { + serde_qs::from_str::<'_, ServerFnUrlError>(v.as_ref()).ok() + } else { + None + }; + leptos::logging::log!("Parsed query key {k} with value {foo:?}"); + foo + }) + .collect() } pub fn referrer_to_url(referer: &HeaderValue, fn_name: &str) -> Option<Url> { @@ -185,9 +202,12 @@ 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{error: error.to_owned(), fn_name: fn_name.to_owned()}) - .expect("Could not serialize server fn error!") - .as_str(), + serde_qs::to_string(&ServerFnUrlError::new( + fn_name.to_owned(), + error.to_owned(), + )) + .expect("Could not serialize server fn error!") + .as_str(), ); self diff --git a/router/src/components/form.rs b/router/src/components/form.rs index 2177c442eb..813e8786ca 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -2,9 +2,9 @@ use crate::{ hooks::has_router, use_navigate, use_resolved_path, NavigateOptions, ToHref, Url, }; -use leptos::{html::form, logging::*, *}; +use leptos::{html::form, logging::*, server_fn::error::ServerFnUrlError, *}; use serde::{de::DeserializeOwned, Serialize}; -use std::{error::Error, rc::Rc}; +use std::{collections::HashSet, error::Error, rc::Rc}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; use wasm_bindgen_futures::JsFuture; use web_sys::RequestRedirect; @@ -450,6 +450,20 @@ where let version = action.version(); let value = action.value(); let input = action.input(); + let errors = use_context::<HashSet<ServerFnUrlError>>(); + + if let (Some(url_error), Some(error)) = ( + errors + .map(|errors| { + errors + .into_iter() + .find(|e| e.fn_name().contains(&action_url)) + }) + .flatten(), + error, + ) { + error.try_set(Some(Box::new(ServerFnErrorErr::from(url_error)))); + } let on_error = Rc::new(move |e: &gloo_net::Error| { batch(move || { diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index d345df6b7d..24c5ba0e61 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -54,7 +54,7 @@ impl From<ServerFnError> for Error { /// Unlike [`ServerFnErrorErr`], this does not implement [`std::error::Error`]. /// This means that other error types can easily be converted into it using the /// `?` operator. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(tag = "type", content = "message")] pub enum ServerFnError { /// Error while trying to register the server function (only occurs in case of poisoned RwLock). @@ -73,6 +73,45 @@ pub enum ServerFnError { MissingArg(String), } +/// TODO: Write Documentation +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct ServerFnUrlError { + internal_error: ServerFnError, + internal_fn_name: String, +} + +impl ServerFnUrlError { + /// TODO: Write Documentation + pub fn new(fn_name: String, error: ServerFnError) -> 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<ServerFnUrlError> for ServerFnError { + fn from(error: ServerFnUrlError) -> Self { + error.internal_error + } +} + +impl From<ServerFnUrlError> 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!( From 15cb0780ee960d46bc5a3fa717e88462e3eaaa7c Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Mon, 8 Jan 2024 20:29:06 -0500 Subject: [PATCH 13/22] Make error gettable by server rendered action form --- .../action-form-error-handling/src/app.rs | 20 +++++++-- integrations/actix/src/lib.rs | 22 ++++++++-- integrations/utils/src/lib.rs | 38 +++++++++++++---- router/src/components/form.rs | 18 +++++++- server_fn/src/error.rs | 41 ++++++++++++++++++- 5 files changed, 121 insertions(+), 18 deletions(-) diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs index 8057b9c30f..eaac0e7a34 100644 --- a/examples/action-form-error-handling/src/app.rs +++ b/examples/action-form-error-handling/src/app.rs @@ -41,14 +41,28 @@ async fn do_something(should_error: Option<String>) -> Result<String, ServerFnEr /// Renders the home page of your application. #[component] fn HomePage() -> impl IntoView { - let do_something_action = create_server_action::<DoSomething>(); + let do_something_action = Action::<DoSomething, _>::server(); + let error = RwSignal::new(None); + + Effect::new_isomorphic(move |_| { + with!(|error| { + logging::log!("Got error in app: {error:?}"); + }) + }); view! { <h1>"Test the action form!"</h1> - <ErrorBoundary fallback=move |error| format!("{:#?}", error.get().into_iter().next().unwrap().1.into_inner().to_string())> + <ErrorBoundary fallback=move |error| format!("{:#?}", error + .get() + .into_iter() + .next() + .unwrap() + .1.into_inner() + .to_string()) + > {do_something_action.value()} </ErrorBoundary> - <ActionForm action=do_something_action class="form"> + <ActionForm action=do_something_action class="form" error=error> <label>Should error: <input type="checkbox" name="should_error"/></label> <button type="submit">Submit</button> </ActionForm> diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index a29e3434b2..9d7b422183 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -25,7 +25,8 @@ use leptos::{ *, }; use leptos_integration_utils::{ - build_async_response, html_parts_separated, referrer_to_url, WithServerFn, + build_async_response, filter_server_fn_url_errors, html_parts_separated, + referrer_to_url, WithServerFn, }; use leptos_meta::*; use leptos_router::*; @@ -300,13 +301,19 @@ pub fn handle_server_fns_with_context( let url = req .headers() .get(header::REFERER) - .and_then(|referrer| referrer_to_url(referrer, fn_name.as_str())); + .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(&e, fn_name.as_str()).as_str(), + url.with_server_fn( + &e, + fn_name.as_str(), + ) + .as_str(), )) .finish() } else { @@ -754,6 +761,15 @@ fn provide_contexts(req: &HttpRequest, res_options: ResponseOptions) { provide_context(MetaContext::new()); provide_context(res_options); provide_context(req.clone()); + if let Some(referrer) = req.headers().get(header::REFERER) { + leptos::logging::log!("Referrer = {referrer:?}"); + provide_context(filter_server_fn_url_errors( + referrer + .to_str() + .expect("Invalid referer header from browser request"), + )); + } + provide_server_redirect(redirect); #[cfg(feature = "nonce")] leptos::nonce::provide_nonce(); diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs index f8f38adfee..044df763e8 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -1,10 +1,13 @@ use futures::{Stream, StreamExt}; use http::HeaderValue; -use leptos::{nonce::use_nonce, use_context, RuntimeId, ServerFnError}; +use leptos::{ + nonce::use_nonce, server_fn::error::ServerFnUrlError, use_context, + RuntimeId, ServerFnError, +}; use leptos_config::LeptosOptions; use leptos_meta::MetaContext; use regex::Regex; -use serde::{Serialize, Deserialize}; +use std::collections::HashSet; use url::Url; extern crate tracing; @@ -162,10 +165,24 @@ pub async fn build_async_response( format!("{head}<body{body_meta}>{buf}{tail}") } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ServerFnUrlError { - pub error: ServerFnError, - pub fn_name: String +pub fn filter_server_fn_url_errors<'a>( + referrer: impl Into<&'a str>, +) -> HashSet<ServerFnUrlError> { + Url::parse(referrer.into()) + .expect("Cannot parse referrer from page request") + .query_pairs() + .into_iter() + .filter_map(|(k, v)| { + + let foo = if k.starts_with("server_fn_error_") { + serde_qs::from_str::<'_, ServerFnUrlError>(v.as_ref()).ok() + } else { + None + }; + leptos::logging::log!("Parsed query key {k} with value {foo:?}"); + foo + }) + .collect() } pub fn referrer_to_url(referer: &HeaderValue, fn_name: &str) -> Option<Url> { @@ -185,9 +202,12 @@ 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{error: error.to_owned(), fn_name: fn_name.to_owned()}) - .expect("Could not serialize server fn error!") - .as_str(), + serde_qs::to_string(&ServerFnUrlError::new( + fn_name.to_owned(), + error.to_owned(), + )) + .expect("Could not serialize server fn error!") + .as_str(), ); self diff --git a/router/src/components/form.rs b/router/src/components/form.rs index 2177c442eb..d3402ef5e4 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -2,9 +2,9 @@ use crate::{ hooks::has_router, use_navigate, use_resolved_path, NavigateOptions, ToHref, Url, }; -use leptos::{html::form, logging::*, *}; +use leptos::{html::form, logging::*, server_fn::error::ServerFnUrlError, *}; use serde::{de::DeserializeOwned, Serialize}; -use std::{error::Error, rc::Rc}; +use std::{collections::HashSet, error::Error, rc::Rc}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; use wasm_bindgen_futures::JsFuture; use web_sys::RequestRedirect; @@ -450,6 +450,20 @@ where let version = action.version(); let value = action.value(); let input = action.input(); + let errors = use_context::<HashSet<ServerFnUrlError>>(); + + if let (Some(url_error), Some(error)) = ( + errors + .map(|errors| { + errors + .into_iter() + .find(|e| action_url.contains(e.fn_name())) + }) + .flatten(), + error, + ) { + error.try_set(Some(Box::new(ServerFnErrorErr::from(url_error)))); + } let on_error = Rc::new(move |e: &gloo_net::Error| { batch(move || { diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index d345df6b7d..24c5ba0e61 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -54,7 +54,7 @@ impl From<ServerFnError> for Error { /// Unlike [`ServerFnErrorErr`], this does not implement [`std::error::Error`]. /// This means that other error types can easily be converted into it using the /// `?` operator. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(tag = "type", content = "message")] pub enum ServerFnError { /// Error while trying to register the server function (only occurs in case of poisoned RwLock). @@ -73,6 +73,45 @@ pub enum ServerFnError { MissingArg(String), } +/// TODO: Write Documentation +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct ServerFnUrlError { + internal_error: ServerFnError, + internal_fn_name: String, +} + +impl ServerFnUrlError { + /// TODO: Write Documentation + pub fn new(fn_name: String, error: ServerFnError) -> 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<ServerFnUrlError> for ServerFnError { + fn from(error: ServerFnUrlError) -> Self { + error.internal_error + } +} + +impl From<ServerFnUrlError> 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!( From 463f77874f3b97306aace3d3d39c4d2084b71dcf Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Thu, 11 Jan 2024 18:31:26 -0500 Subject: [PATCH 14/22] Rearrange query string errors logic --- integrations/actix/src/lib.rs | 14 ++++++-------- integrations/utils/src/lib.rs | 21 --------------------- leptos/src/lib.rs | 2 +- router/src/components/form.rs | 16 +++++++++------- server_fn/Cargo.toml | 1 + server_fn/src/lib.rs | 23 ++++++++++++++++++++++- 6 files changed, 39 insertions(+), 38 deletions(-) diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 9d7b422183..42f5d0d09e 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -20,12 +20,12 @@ 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, filter_server_fn_url_errors, html_parts_separated, + build_async_response, html_parts_separated, referrer_to_url, WithServerFn, }; use leptos_meta::*; @@ -761,12 +761,10 @@ fn provide_contexts(req: &HttpRequest, res_options: ResponseOptions) { provide_context(MetaContext::new()); provide_context(res_options); provide_context(req.clone()); - if let Some(referrer) = req.headers().get(header::REFERER) { - leptos::logging::log!("Referrer = {referrer:?}"); - provide_context(filter_server_fn_url_errors( - referrer - .to_str() - .expect("Invalid referer header from browser request"), + if let Some(query) = req.uri().query() { + leptos::logging::log!("query = {query}"); + provide_context(query_to_errors( + query )); } diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs index 044df763e8..0ddd7a628d 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -7,7 +7,6 @@ use leptos::{ use leptos_config::LeptosOptions; use leptos_meta::MetaContext; use regex::Regex; -use std::collections::HashSet; use url::Url; extern crate tracing; @@ -165,26 +164,6 @@ pub async fn build_async_response( format!("{head}<body{body_meta}>{buf}{tail}") } -pub fn filter_server_fn_url_errors<'a>( - referrer: impl Into<&'a str>, -) -> HashSet<ServerFnUrlError> { - Url::parse(referrer.into()) - .expect("Cannot parse referrer from page request") - .query_pairs() - .into_iter() - .filter_map(|(k, v)| { - - let foo = if k.starts_with("server_fn_error_") { - serde_qs::from_str::<'_, ServerFnUrlError>(v.as_ref()).ok() - } else { - None - }; - leptos::logging::log!("Parsed query key {k} with value {foo:?}"); - foo - }) - .collect() -} - pub fn referrer_to_url(referer: &HeaderValue, fn_name: &str) -> Option<Url> { Url::parse( &Regex::new(&format!(r"(?:\?|&)?server_fn_error_{fn_name}=[^&]+")) diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index 0d208a21a4..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, ServerFn as _}; +pub use server_fn::{self, query_to_errors, ServerFn as _}; mod error_boundary; pub use error_boundary::*; mod animated_show; diff --git a/router/src/components/form.rs b/router/src/components/form.rs index d3402ef5e4..24435a2b77 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -450,20 +450,22 @@ where let version = action.version(); let value = action.value(); let input = action.input(); - let errors = use_context::<HashSet<ServerFnUrlError>>(); - if let (Some(url_error), Some(error)) = ( + let effect_action_url = action_url.clone(); + Effect::new_isomorphic(move |_| { + let errors = use_context::<HashSet<ServerFnUrlError>>(); + if let Some(url_error) = errors .map(|errors| { errors .into_iter() - .find(|e| action_url.contains(e.fn_name())) + .find(|e| effect_action_url.contains(e.fn_name())) }) - .flatten(), - error, - ) { - error.try_set(Some(Box::new(ServerFnErrorErr::from(url_error)))); + .flatten() { + leptos::logging::log!("In iso effect with error = {url_error:?}"); + value.try_set(Some(Err(url_error.error().clone()))); } + }); let on_error = Rc::new(move |e: &gloo_net::Error| { batch(move || { diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index c85e6aa6c5..b971800ca7 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -22,6 +22,7 @@ xxhash-rust = { version = "0.8", features = ["const_xxh64"] } const_format = "0.2" inventory = { version = "0.3", optional = true } lazy_static = "1" +url = "2.5.0" [dev-dependencies] server_fn = { version = "0.2" } diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index e677f9c672..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)] @@ -95,7 +96,8 @@ use quote::TokenStreamExt; pub use serde; use serde::{de::DeserializeOwned, Serialize}; pub use server_fn_macro_default::server; -use std::{future::Future, pin::Pin, str::FromStr}; +use url::Url; +use std::{future::Future, pin::Pin, str::FromStr, collections::HashSet}; #[cfg(any(feature = "ssr", doc))] use syn::parse_quote; // used by the macro @@ -619,3 +621,22 @@ fn get_server_url() -> &'static str { .get() .expect("Call set_root_url before calling a server function.") } + +#[doc(hidden)] +pub fn query_to_errors(query: &str) -> HashSet<ServerFnUrlError> { + // 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. + Url::parse(&format!("http://base.com?{query}")) + .expect("Cannot parse referrer from page request") + .query_pairs() + .into_iter() + .filter_map(|(k, v)| { + if k.starts_with("server_fn_error_") { + serde_qs::from_str::<'_, ServerFnUrlError>(v.as_ref()).ok() + } else { + None + } + }) + .collect() +} From 5b056a7f452ff75f198171b2e77efe84e78bf775 Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Thu, 11 Jan 2024 22:14:05 -0500 Subject: [PATCH 15/22] Add a way to detect if server fn is coming from a client with wasm disabled --- .../action-form-error-handling/src/app.rs | 16 +++--- integrations/actix/src/lib.rs | 9 +++- router/src/components/form.rs | 51 ++++++++++++++----- 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs index eaac0e7a34..6989756fcc 100644 --- a/examples/action-form-error-handling/src/app.rs +++ b/examples/action-form-error-handling/src/app.rs @@ -42,12 +42,10 @@ async fn do_something(should_error: Option<String>) -> Result<String, ServerFnEr #[component] fn HomePage() -> impl IntoView { let do_something_action = Action::<DoSomething, _>::server(); - let error = RwSignal::new(None); + let value = Signal::derive(move || do_something_action.value().get().unwrap_or_else(|| Ok(String::new()))); Effect::new_isomorphic(move |_| { - with!(|error| { - logging::log!("Got error in app: {error:?}"); - }) + logging::log!("Got value = {:?}", value.get()); }); view! { @@ -60,12 +58,12 @@ fn HomePage() -> impl IntoView { .1.into_inner() .to_string()) > - {do_something_action.value()} + {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> - <ActionForm action=do_something_action class="form" error=error> - <label>Should error: <input type="checkbox" name="should_error"/></label> - <button type="submit">Submit</button> - </ActionForm> } } diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 42f5d0d09e..6301861618 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -230,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 = @@ -246,6 +248,7 @@ pub fn handle_server_fns_with_context( != 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 @@ -264,7 +267,7 @@ pub fn handle_server_fns_with_context( )) .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); @@ -282,16 +285,19 @@ pub fn handle_server_fns_with_context( 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", ); res.body(data) } Payload::Json(data) => { + leptos::logging::log!("serverfn return json = {data:?}"); res.content_type("application/json"); res.body(data) } @@ -324,6 +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 diff --git a/router/src/components/form.rs b/router/src/components/form.rs index 24435a2b77..b1bbb70eef 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -432,7 +432,7 @@ pub fn ActionForm<I, O>( #[prop(optional, into)] attributes: Vec<(&'static str, Attribute)>, /// Component children; should include the HTML of the form elements. - children: Children, + children: ChildrenFn, ) -> impl IntoView where I: Clone + ServerFn + 'static, @@ -452,19 +452,46 @@ where let input = action.input(); let effect_action_url = action_url.clone(); - Effect::new_isomorphic(move |_| { - let errors = use_context::<HashSet<ServerFnUrlError>>(); + + let children = Box::new(move || { + let wasm_has_loaded = RwSignal::new(false); + let children = StoredValue::new(children); + let action_url = effect_action_url.clone(); + + Effect::new_isomorphic(move |_| { + let errors = use_context::<HashSet<ServerFnUrlError>>(); if let Some(url_error) = - errors - .map(|errors| { errors - .into_iter() - .find(|e| effect_action_url.contains(e.fn_name())) - }) - .flatten() { - leptos::logging::log!("In iso effect with error = {url_error:?}"); - value.try_set(Some(Err(url_error.error().clone()))); - } + .map(|errors| { + errors + .into_iter() + .find(|e| effect_action_url.contains(e.fn_name())) + }) + .flatten() { + leptos::logging::log!("In iso effect with error = {url_error:?}"); + value.try_set(Some(Err(url_error.error().clone()))); + } + }); + + Effect::new(move |_| { + leptos::logging::log!("In browser action form effect"); + wasm_has_loaded.set(true); + }); + + view!{ + <input + id={format!("leptos_wasm_has_loaded_{}", action_url.split('/').last().unwrap_or(""))} + name="leptos_wasm_has_loaded" + type="hidden" + value=move || with!(|wasm_has_loaded| + if *wasm_has_loaded { + "true" + } else { + "false" + }) + /> + {with!(|children| children())} + } }); let on_error = Rc::new(move |e: &gloo_net::Error| { From 1472aaa0a389561146ab241041043a5f8d46b063 Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Fri, 12 Jan 2024 20:59:34 -0500 Subject: [PATCH 16/22] Work on handling server fns --- .../action-form-error-handling/src/app.rs | 3 +- integrations/actix/src/lib.rs | 157 +++++++++++------- integrations/axum/src/lib.rs | 2 +- integrations/utils/src/lib.rs | 73 +++++--- integrations/viz/src/lib.rs | 2 +- leptos/src/lib.rs | 5 +- router/src/components/form.rs | 30 ++-- server_fn/src/error.rs | 39 ----- server_fn/src/lib.rs | 46 ++++- 9 files changed, 215 insertions(+), 142 deletions(-) diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs index 6989756fcc..ab935e4485 100644 --- a/examples/action-form-error-handling/src/app.rs +++ b/examples/action-form-error-handling/src/app.rs @@ -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> + } } diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 6301861618..54b9311c81 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -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::*; @@ -36,7 +35,7 @@ use std::{ fmt::{Debug, Display}, future::Future, pin::Pin, - sync::Arc, + sync::{Arc, OnceLock}, }; #[cfg(debug_assertions)] use tracing::instrument; @@ -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]. @@ -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 @@ -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 = @@ -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(), ) @@ -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 @@ -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")] diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index d442d3780a..57b3390575 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(&e, fn_name.as_str()).as_str(), + referer.with_server_fn_error(&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 0ddd7a628d..fca64f2fec 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -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; @@ -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 +} diff --git a/integrations/viz/src/lib.rs b/integrations/viz/src/lib.rs index 7a5c48a043..04828ef1a4 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(&e, fn_name.as_str()).as_str(), + url.with_server_fn_error(&e, fn_name.as_str()).as_str(), ) .body(Default::default()) } else { diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index b48bf2622a..5c32d8a1fc 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_errors, ServerFn as _}; +pub use server_fn::{self, query_to_responses, ServerFn as _}; mod error_boundary; pub use error_boundary::*; mod animated_show; @@ -352,3 +352,6 @@ 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 b1bbb70eef..1509bfe5e6 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -2,7 +2,12 @@ use crate::{ hooks::has_router, use_navigate, use_resolved_path, NavigateOptions, ToHref, Url, }; -use leptos::{html::form, logging::*, server_fn::error::ServerFnUrlError, *}; +use leptos::{ + html::form, + logging::*, + server_fn::ServerFnUrlResponse, + *, +}; use serde::{de::DeserializeOwned, Serialize}; use std::{collections::HashSet, error::Error, rc::Rc}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; @@ -459,17 +464,16 @@ where let action_url = effect_action_url.clone(); Effect::new_isomorphic(move |_| { - let errors = use_context::<HashSet<ServerFnUrlError>>(); - if let Some(url_error) = - errors - .map(|errors| { - errors + let results = use_context::<HashSet<ServerFnUrlResponse<O>>>(); + if let Some(result) = results + .map(|results| { + results .into_iter() - .find(|e| effect_action_url.contains(e.fn_name())) + .find(|e| effect_action_url.contains(e.name())) }) - .flatten() { - leptos::logging::log!("In iso effect with error = {url_error:?}"); - value.try_set(Some(Err(url_error.error().clone()))); + .flatten() + { + value.try_set(Some(result.get())); } }); @@ -478,10 +482,10 @@ where wasm_has_loaded.set(true); }); - view!{ + view! { <input - id={format!("leptos_wasm_has_loaded_{}", action_url.split('/').last().unwrap_or(""))} - name="leptos_wasm_has_loaded" + id={format!("{WASM_LOADED_NAME}_{}", action_url.split('/').last().unwrap_or(""))} + name=WASM_LOADED_NAME type="hidden" value=move || with!(|wasm_has_loaded| if *wasm_has_loaded { diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index 24c5ba0e61..2cef09af68 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -73,45 +73,6 @@ pub enum ServerFnError { MissingArg(String), } -/// TODO: Write Documentation -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] -pub struct ServerFnUrlError { - internal_error: ServerFnError, - internal_fn_name: String, -} - -impl ServerFnUrlError { - /// TODO: Write Documentation - pub fn new(fn_name: String, error: ServerFnError) -> 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<ServerFnUrlError> for ServerFnError { - fn from(error: ServerFnUrlError) -> Self { - error.internal_error - } -} - -impl From<ServerFnUrlError> 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 df4d54dabf..b53d718561 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -82,7 +82,6 @@ // used by the macro #[doc(hidden)] pub use const_format; -use error::ServerFnUrlError; // used by the macro #[cfg(feature = "ssr")] #[doc(hidden)] @@ -94,10 +93,10 @@ use quote::TokenStreamExt; // used by the macro #[doc(hidden)] pub use serde; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{de::DeserializeOwned, Serialize, Deserialize}; pub use server_fn_macro_default::server; use url::Url; -use std::{future::Future, pin::Pin, str::FromStr, collections::HashSet}; +use std::{future::Future, pin::Pin, str::FromStr, collections::HashSet, hash::Hash, cmp::Eq}; #[cfg(any(feature = "ssr", doc))] use syn::parse_quote; // used by the macro @@ -600,6 +599,41 @@ where } } +/// TODO: Write Documentation +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct ServerFnUrlResponse<T: Clone> { + data: Result<T, ServerFnError>, + fn_name: String, +} + +impl<T: Clone> ServerFnUrlResponse<T> { + /// 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<T, ServerFnError> { + 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<reqwest::Client> = @@ -623,7 +657,7 @@ fn get_server_url() -> &'static str { } #[doc(hidden)] -pub fn query_to_errors(query: &str) -> HashSet<ServerFnUrlError> { +pub fn query_to_responses<T: Clone + DeserializeOwned + Hash + Eq>(query: &str) -> HashSet<ServerFnUrlResponse<T>> { // 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. @@ -632,8 +666,8 @@ pub fn query_to_errors(query: &str) -> HashSet<ServerFnUrlError> { .query_pairs() .into_iter() .filter_map(|(k, v)| { - if k.starts_with("server_fn_error_") { - serde_qs::from_str::<'_, ServerFnUrlError>(v.as_ref()).ok() + if k.starts_with("server_fn_response_") { + serde_qs::from_str::<'_, ServerFnUrlResponse<T>>(v.as_ref()).ok() } else { None } From 7d1583a0a0f4d6553102cae79a3a5b00cab7fa0a Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Fri, 12 Jan 2024 23:25:20 -0500 Subject: [PATCH 17/22] Make server fn responses work on both success and failure --- .../action-form-error-handling/src/app.rs | 6 ++-- integrations/actix/Cargo.toml | 1 + integrations/actix/src/lib.rs | 15 ++++----- integrations/utils/src/lib.rs | 2 +- leptos/src/lib.rs | 5 ++- router/src/components/form.rs | 32 +++++++++---------- server_fn/src/lib.rs | 20 ++++++++++-- 7 files changed, 48 insertions(+), 33 deletions(-) diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs index ab935e4485..27ea5fe29d 100644 --- a/examples/action-form-error-handling/src/app.rs +++ b/examples/action-form-error-handling/src/app.rs @@ -44,9 +44,9 @@ 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()); - }); + // Effect::new_isomorphic(move |_| { + // logging::log!("Got value = {:?}", value.get()); + // }); view! { <h1>"Test the action form!"</h1> diff --git a/integrations/actix/Cargo.toml b/integrations/actix/Cargo.toml index 8c21885a11..1809b66f42 100644 --- a/integrations/actix/Cargo.toml +++ b/integrations/actix/Cargo.toml @@ -20,6 +20,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 54b9311c81..ad72407046 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -20,7 +20,7 @@ use actix_web::{ use futures::{Stream, StreamExt}; use leptos::{ leptos_server::{server_fn_by_path, Payload}, - server_fn::Encoding, + server_fn::{Encoding, ServerFnContext}, ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement, *, }; @@ -351,7 +351,7 @@ pub fn handle_server_fns_with_context( HttpResponse::SeeOther() .insert_header(( header::LOCATION, - url.with_server_fn_error( + <url::Url as WithServerFn<'_, ()>>::with_server_fn_error(url, &e, fn_name.as_str(), ) @@ -804,13 +804,10 @@ 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(ServerFnContext::new(String::from(query))); + } provide_server_redirect(redirect); #[cfg(feature = "nonce")] diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs index fca64f2fec..73f0c8ebdd 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -167,7 +167,7 @@ pub async fn build_async_response( pub fn referrer_to_url(referer: &str, fn_name: &str) -> Url { Url::parse( - &Regex::new(&format!(r"(?:\?|&)?server_fn_error_{fn_name}=[^&]+")) + &Regex::new(&format!(r"(?:\?|&)?server_fn_response_{fn_name}=[^&]+")) .unwrap() .replace(referer, ""), ) diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index 5c32d8a1fc..a300d8dbdd 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -187,7 +187,10 @@ 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_responses, ServerFn as _, ServerFnContext, + ServerFnUrlResponse, +}; mod error_boundary; pub use error_boundary::*; mod animated_show; diff --git a/router/src/components/form.rs b/router/src/components/form.rs index 1509bfe5e6..e47da6a25e 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -2,14 +2,9 @@ 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::*, *}; use serde::{de::DeserializeOwned, Serialize}; -use std::{collections::HashSet, error::Error, rc::Rc}; +use std::{error::Error, rc::Rc}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; use wasm_bindgen_futures::JsFuture; use web_sys::RequestRedirect; @@ -464,16 +459,19 @@ where let action_url = effect_action_url.clone(); Effect::new_isomorphic(move |_| { - let results = use_context::<HashSet<ServerFnUrlResponse<O>>>(); - if let Some(result) = results - .map(|results| { - results - .into_iter() - .find(|e| effect_action_url.contains(e.name())) - }) - .flatten() - { - value.try_set(Some(result.get())); + let context = use_context::<ServerFnContext>(); + + if let Some(context) = context { + leptos::logging::log!("Got context in iso effect with query = {}", context.get_query()); + if let Some(result) = query_to_responses::<O>( + context.get_query() + ) + .into_iter() + .find(|r| effect_action_url.contains(r.name())) + { + leptos::logging::log!("iso effefct got match!"); + value.try_set(Some(result.get())); + } } }); diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index b53d718561..e478078e73 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -96,7 +96,7 @@ pub use serde; use serde::{de::DeserializeOwned, Serialize, Deserialize}; 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, hash::Hash, cmp::Eq}; #[cfg(any(feature = "ssr", doc))] use syn::parse_quote; // used by the macro @@ -599,6 +599,22 @@ where } } +#[doc(hidden)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerFnContext(String); + +impl ServerFnContext { + pub fn new(query: String) -> Self { + Self(query) + } + + pub fn get_query(&self) -> &str { + &self.0 + } +} + + + /// TODO: Write Documentation #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub struct ServerFnUrlResponse<T: Clone> { @@ -657,7 +673,7 @@ fn get_server_url() -> &'static str { } #[doc(hidden)] -pub fn query_to_responses<T: Clone + DeserializeOwned + Hash + Eq>(query: &str) -> HashSet<ServerFnUrlResponse<T>> { +pub fn query_to_responses<T: Clone + DeserializeOwned + Serialize + 'static>(query: &str) -> Vec<ServerFnUrlResponse<T>> { // 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. From 487b356ece0e5bf47e9c0cc05b5f77f29ad98b71 Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Fri, 12 Jan 2024 23:30:34 -0500 Subject: [PATCH 18/22] Uncomment iso effect --- examples/action-form-error-handling/src/app.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs index 27ea5fe29d..ab935e4485 100644 --- a/examples/action-form-error-handling/src/app.rs +++ b/examples/action-form-error-handling/src/app.rs @@ -44,9 +44,9 @@ 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()); - // }); + Effect::new_isomorphic(move |_| { + logging::log!("Got value = {:?}", value.get()); + }); view! { <h1>"Test the action form!"</h1> From 90f5dac6c8dfeea30276b50de81f1ad8abcddf9a Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Sat, 13 Jan 2024 13:02:47 -0500 Subject: [PATCH 19/22] Revert "Uncomment iso effect" This reverts commit 487b356ece0e5bf47e9c0cc05b5f77f29ad98b71. --- examples/action-form-error-handling/src/app.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs index ab935e4485..27ea5fe29d 100644 --- a/examples/action-form-error-handling/src/app.rs +++ b/examples/action-form-error-handling/src/app.rs @@ -44,9 +44,9 @@ 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()); - }); + // Effect::new_isomorphic(move |_| { + // logging::log!("Got value = {:?}", value.get()); + // }); view! { <h1>"Test the action form!"</h1> From d4f7fd87821333039ac37cde534fe9e124429d22 Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Sat, 13 Jan 2024 13:02:47 -0500 Subject: [PATCH 20/22] Revert "Make server fn responses work on both success and failure" This reverts commit 7d1583a0a0f4d6553102cae79a3a5b00cab7fa0a. --- .../action-form-error-handling/src/app.rs | 6 ++-- integrations/actix/Cargo.toml | 1 - integrations/actix/src/lib.rs | 15 +++++---- integrations/utils/src/lib.rs | 2 +- leptos/src/lib.rs | 5 +-- router/src/components/form.rs | 32 ++++++++++--------- server_fn/src/lib.rs | 20 ++---------- 7 files changed, 33 insertions(+), 48 deletions(-) diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs index 27ea5fe29d..ab935e4485 100644 --- a/examples/action-form-error-handling/src/app.rs +++ b/examples/action-form-error-handling/src/app.rs @@ -44,9 +44,9 @@ 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()); - // }); + Effect::new_isomorphic(move |_| { + logging::log!("Got value = {:?}", value.get()); + }); view! { <h1>"Test the action form!"</h1> diff --git a/integrations/actix/Cargo.toml b/integrations/actix/Cargo.toml index 1809b66f42..8c21885a11 100644 --- a/integrations/actix/Cargo.toml +++ b/integrations/actix/Cargo.toml @@ -20,7 +20,6 @@ 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 ad72407046..54b9311c81 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -20,7 +20,7 @@ use actix_web::{ use futures::{Stream, StreamExt}; use leptos::{ leptos_server::{server_fn_by_path, Payload}, - server_fn::{Encoding, ServerFnContext}, + server_fn::Encoding, ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement, *, }; @@ -351,7 +351,7 @@ pub fn handle_server_fns_with_context( HttpResponse::SeeOther() .insert_header(( header::LOCATION, - <url::Url as WithServerFn<'_, ()>>::with_server_fn_error(url, + url.with_server_fn_error( &e, fn_name.as_str(), ) @@ -804,10 +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(ServerFnContext::new(String::from(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")] diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs index 73f0c8ebdd..fca64f2fec 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -167,7 +167,7 @@ pub async fn build_async_response( pub fn referrer_to_url(referer: &str, fn_name: &str) -> Url { Url::parse( - &Regex::new(&format!(r"(?:\?|&)?server_fn_response_{fn_name}=[^&]+")) + &Regex::new(&format!(r"(?:\?|&)?server_fn_error_{fn_name}=[^&]+")) .unwrap() .replace(referer, ""), ) diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index a300d8dbdd..5c32d8a1fc 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -187,10 +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 _, ServerFnContext, - ServerFnUrlResponse, -}; +pub use server_fn::{self, query_to_responses, ServerFn as _}; mod error_boundary; pub use error_boundary::*; mod animated_show; diff --git a/router/src/components/form.rs b/router/src/components/form.rs index e47da6a25e..1509bfe5e6 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -2,9 +2,14 @@ use crate::{ hooks::has_router, use_navigate, use_resolved_path, NavigateOptions, ToHref, Url, }; -use leptos::{html::form, logging::*, *}; +use leptos::{ + html::form, + logging::*, + server_fn::ServerFnUrlResponse, + *, +}; use serde::{de::DeserializeOwned, Serialize}; -use std::{error::Error, rc::Rc}; +use std::{collections::HashSet, error::Error, rc::Rc}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; use wasm_bindgen_futures::JsFuture; use web_sys::RequestRedirect; @@ -459,19 +464,16 @@ where let action_url = effect_action_url.clone(); Effect::new_isomorphic(move |_| { - let context = use_context::<ServerFnContext>(); - - if let Some(context) = context { - leptos::logging::log!("Got context in iso effect with query = {}", context.get_query()); - if let Some(result) = query_to_responses::<O>( - context.get_query() - ) - .into_iter() - .find(|r| effect_action_url.contains(r.name())) - { - leptos::logging::log!("iso effefct got match!"); - value.try_set(Some(result.get())); - } + let results = use_context::<HashSet<ServerFnUrlResponse<O>>>(); + if let Some(result) = results + .map(|results| { + results + .into_iter() + .find(|e| effect_action_url.contains(e.name())) + }) + .flatten() + { + value.try_set(Some(result.get())); } }); diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index e478078e73..b53d718561 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -96,7 +96,7 @@ pub use serde; use serde::{de::DeserializeOwned, Serialize, Deserialize}; pub use server_fn_macro_default::server; use url::Url; -use std::{future::Future, pin::Pin, str::FromStr, hash::Hash, cmp::Eq}; +use std::{future::Future, pin::Pin, str::FromStr, collections::HashSet, hash::Hash, cmp::Eq}; #[cfg(any(feature = "ssr", doc))] use syn::parse_quote; // used by the macro @@ -599,22 +599,6 @@ where } } -#[doc(hidden)] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ServerFnContext(String); - -impl ServerFnContext { - pub fn new(query: String) -> Self { - Self(query) - } - - pub fn get_query(&self) -> &str { - &self.0 - } -} - - - /// TODO: Write Documentation #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub struct ServerFnUrlResponse<T: Clone> { @@ -673,7 +657,7 @@ fn get_server_url() -> &'static str { } #[doc(hidden)] -pub fn query_to_responses<T: Clone + DeserializeOwned + Serialize + 'static>(query: &str) -> Vec<ServerFnUrlResponse<T>> { +pub fn query_to_responses<T: Clone + DeserializeOwned + Hash + Eq>(query: &str) -> HashSet<ServerFnUrlResponse<T>> { // 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. From c00a696d9bc7dd7ad5f673bc6fb06a9b168d1bca Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Sat, 13 Jan 2024 13:09:45 -0500 Subject: [PATCH 21/22] Revert "Work on handling server fns" This reverts commit 1472aaa0a389561146ab241041043a5f8d46b063. --- .../action-form-error-handling/src/app.rs | 3 +- integrations/actix/src/lib.rs | 157 +++++++----------- integrations/axum/src/lib.rs | 2 +- integrations/utils/src/lib.rs | 73 +++----- integrations/viz/src/lib.rs | 2 +- leptos/src/lib.rs | 5 +- router/src/components/form.rs | 30 ++-- server_fn/src/error.rs | 39 +++++ server_fn/src/lib.rs | 46 +---- 9 files changed, 142 insertions(+), 215 deletions(-) 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} - </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> } } 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<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]. @@ -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}<body{body_meta}>{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> { 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::<T>::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<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!") +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::<HashSet<ServerFnUrlResponse<O>>>(); - if let Some(result) = results - .map(|results| { - results + let errors = use_context::<HashSet<ServerFnUrlError>>(); + 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!{ <input - id={format!("{WASM_LOADED_NAME}_{}", action_url.split('/').last().unwrap_or(""))} - name=WASM_LOADED_NAME + id={format!("leptos_wasm_has_loaded_{}", action_url.split('/').last().unwrap_or(""))} + name="leptos_wasm_has_loaded" type="hidden" value=move || with!(|wasm_has_loaded| if *wasm_has_loaded { diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index 2cef09af68..24c5ba0e61 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -73,6 +73,45 @@ pub enum ServerFnError { MissingArg(String), } +/// TODO: Write Documentation +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct ServerFnUrlError { + internal_error: ServerFnError, + internal_fn_name: String, +} + +impl ServerFnUrlError { + /// TODO: Write Documentation + pub fn new(fn_name: String, error: ServerFnError) -> 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<ServerFnUrlError> for ServerFnError { + fn from(error: ServerFnUrlError) -> Self { + error.internal_error + } +} + +impl From<ServerFnUrlError> 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<T: Clone> { - data: Result<T, ServerFnError>, - fn_name: String, -} - -impl<T: Clone> ServerFnUrlResponse<T> { - /// 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<T, ServerFnError> { - 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<reqwest::Client> = @@ -657,7 +623,7 @@ fn get_server_url() -> &'static str { } #[doc(hidden)] -pub fn query_to_responses<T: Clone + DeserializeOwned + Hash + Eq>(query: &str) -> HashSet<ServerFnUrlResponse<T>> { +pub fn query_to_errors(query: &str) -> HashSet<ServerFnUrlError> { // 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<T: Clone + DeserializeOwned + Hash + Eq>(query: &str) .query_pairs() .into_iter() .filter_map(|(k, v)| { - if k.starts_with("server_fn_response_") { - serde_qs::from_str::<'_, ServerFnUrlResponse<T>>(v.as_ref()).ok() + if k.starts_with("server_fn_error_") { + serde_qs::from_str::<'_, ServerFnUrlError>(v.as_ref()).ok() } else { None } From 29df4a57faa11cb84d9ca995c4e2d086d9c999be Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <insomnia-void@protonmail.com> Date: Sun, 14 Jan 2024 00:15:31 -0500 Subject: [PATCH 22/22] Go further --- examples/action-form-error-handling/src/app.rs | 2 +- integrations/actix/src/lib.rs | 4 ++-- integrations/utils/src/lib.rs | 7 +++---- leptos/src/lib.rs | 3 +++ router/src/components/form.rs | 6 +++--- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs index 6989756fcc..99524d70ce 100644 --- a/examples/action-form-error-handling/src/app.rs +++ b/examples/action-form-error-handling/src/app.rs @@ -59,11 +59,11 @@ 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> } } diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 6301861618..970dfde7d5 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -307,8 +307,8 @@ pub fn handle_server_fns_with_context( let url = req .headers() .get(header::REFERER) - .and_then(|referrer| { - referrer_to_url(referrer, fn_name.as_str()) + .map(|referrer| { + referrer_to_url(referrer.to_str(), fn_name.as_str()) }); if let Some(url) = url { diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs index 0ddd7a628d..3886127831 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -1,5 +1,4 @@ use futures::{Stream, StreamExt}; -use http::HeaderValue; use leptos::{ nonce::use_nonce, server_fn::error::ServerFnUrlError, use_context, RuntimeId, ServerFnError, @@ -164,13 +163,13 @@ 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 { diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index b48bf2622a..b0b47202e2 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -352,3 +352,6 @@ 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 b1bbb70eef..08d7b29e19 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -469,7 +469,7 @@ where }) .flatten() { leptos::logging::log!("In iso effect with error = {url_error:?}"); - value.try_set(Some(Err(url_error.error().clone()))); + value.set(Some(Err(url_error.error().clone()))); } }); @@ -480,8 +480,8 @@ where view!{ <input - id={format!("leptos_wasm_has_loaded_{}", action_url.split('/').last().unwrap_or(""))} - name="leptos_wasm_has_loaded" + id={format!("{WASM_LOADED_NAME}_{}", action_url.split('/').last().unwrap_or(""))} + name=WASM_LOADED_NAME type="hidden" value=move || with!(|wasm_has_loaded| if *wasm_has_loaded {