mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
Update ErrorBoundary to use miette::Diagnostic instead of Error, and various other tweaks (#401)
* Switch RwLock to parking_lot so they are no longer async * cleanup todo_app_sqlite_axum * add errors_axum example --------- Co-authored-by: Indrazar <110272232+Indrazar@users.noreply.github.com>
This commit is contained in:
parent
e2b1365e46
commit
160f336303
22 changed files with 584 additions and 28 deletions
94
examples/errors_axum/Cargo.toml
Normal file
94
examples/errors_axum/Cargo.toml
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
[package]
|
||||||
|
name = "errors_axum"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.66"
|
||||||
|
console_log = "0.2.0"
|
||||||
|
console_error_panic_hook = "0.1.7"
|
||||||
|
futures = "0.3.25"
|
||||||
|
cfg-if = "1.0.0"
|
||||||
|
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||||
|
"serde",
|
||||||
|
] }
|
||||||
|
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
|
||||||
|
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||||
|
leptos_router = { path = "../../../leptos/router", default-features = false }
|
||||||
|
leptos_reactive = { path = "../../../leptos/leptos_reactive", default-features = false }
|
||||||
|
log = "0.4.17"
|
||||||
|
simple_logger = "4.0.0"
|
||||||
|
serde = { version = "1.0.148", features = ["derive"] }
|
||||||
|
serde_json = "1.0.89"
|
||||||
|
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||||
|
reqwest = { version = "0.11.13", features = ["json"] }
|
||||||
|
axum = { version = "0.6.1", optional = true }
|
||||||
|
tower = { version = "0.4.13", optional = true }
|
||||||
|
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
|
||||||
|
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||||
|
http = { version = "0.2.8" }
|
||||||
|
thiserror = "1.0.38"
|
||||||
|
tracing = "0.1.37"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["csr"]
|
||||||
|
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||||
|
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||||
|
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_axum"]
|
||||||
|
|
||||||
|
[package.metadata.cargo-all-features]
|
||||||
|
denylist = [
|
||||||
|
"axum",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
"tokio",
|
||||||
|
"leptos_axum",
|
||||||
|
]
|
||||||
|
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||||
|
|
||||||
|
[package.metadata.leptos]
|
||||||
|
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||||
|
output-name = "errors_axum"
|
||||||
|
# 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 <site-root>/<site-pkg>/app.css
|
||||||
|
style-file = "./style.css"
|
||||||
|
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||||
|
assets-dir = "public"
|
||||||
|
# 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.
|
||||||
|
end2end-cmd = "npx playwright test"
|
||||||
|
# The browserlist query used for optimizing the CSS.
|
||||||
|
browserquery = "defaults"
|
||||||
|
# Set by cargo-leptos watch when building with tha 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
|
21
examples/errors_axum/LICENSE
Normal file
21
examples/errors_axum/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Greg Johnston
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
4
examples/errors_axum/Makefile.toml
Normal file
4
examples/errors_axum/Makefile.toml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[tasks.build]
|
||||||
|
command = "cargo"
|
||||||
|
args = ["+nightly", "build-all-features"]
|
||||||
|
install_crate = "cargo-all-features"
|
42
examples/errors_axum/README.md
Normal file
42
examples/errors_axum/README.md
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# Leptos Todo App Sqlite with Axum
|
||||||
|
|
||||||
|
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
|
||||||
|
|
||||||
|
## Client Side Rendering
|
||||||
|
This example cannot be built as a trunk standalone CSR-only app as it requires the server to send HTTP Status Codes.
|
||||||
|
|
||||||
|
## Server Side Rendering with cargo-leptos
|
||||||
|
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
|
||||||
|
|
||||||
|
1. Install cargo-leptos
|
||||||
|
```bash
|
||||||
|
cargo install --locked cargo-leptos
|
||||||
|
```
|
||||||
|
2. Build the site in watch mode, recompiling on file changes
|
||||||
|
```bash
|
||||||
|
cargo leptos watch
|
||||||
|
```
|
||||||
|
|
||||||
|
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||||
|
|
||||||
|
3. When ready to deploy, run
|
||||||
|
```bash
|
||||||
|
cargo leptos build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server Side Rendering without cargo-leptos
|
||||||
|
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
|
||||||
|
|
||||||
|
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
|
||||||
|
1. Install wasm-pack
|
||||||
|
```bash
|
||||||
|
cargo install wasm-pack
|
||||||
|
```
|
||||||
|
2. Build the Webassembly used to hydrate the HTML from the server
|
||||||
|
```bash
|
||||||
|
wasm-pack build --target=web --debug --no-default-features --features=hydrate
|
||||||
|
```
|
||||||
|
3. Run the server to serve the Webassembly, JS, and HTML
|
||||||
|
```bash
|
||||||
|
cargo run --no-default-features --features=ssr
|
||||||
|
```
|
BIN
examples/errors_axum/public/favicon.ico
Normal file
BIN
examples/errors_axum/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
70
examples/errors_axum/src/error_template.rs
Normal file
70
examples/errors_axum/src/error_template.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
use crate::errors::AppError;
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
use leptos::Errors;
|
||||||
|
use leptos::{
|
||||||
|
component, create_rw_signal, use_context, view, For, ForProps, IntoView, RwSignal, Scope,
|
||||||
|
};
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use leptos_axum::ResponseOptions;
|
||||||
|
|
||||||
|
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
|
||||||
|
// here than just displaying them
|
||||||
|
#[component]
|
||||||
|
pub fn ErrorTemplate(
|
||||||
|
cx: Scope,
|
||||||
|
#[prop(optional)] outside_errors: Option<Errors>,
|
||||||
|
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let errors = match outside_errors {
|
||||||
|
Some(e) => {
|
||||||
|
let errors = create_rw_signal(cx, e);
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
None => match errors {
|
||||||
|
Some(e) => e,
|
||||||
|
None => panic!("No Errors found and we expected errors!"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get Errors from Signal
|
||||||
|
let errors = errors.get().0;
|
||||||
|
|
||||||
|
// Downcast lets us take a type that implements `std::error::Error`
|
||||||
|
let errors: Vec<AppError> = errors
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
|
||||||
|
.flatten()
|
||||||
|
.collect();
|
||||||
|
println!("Errors: {errors:#?}");
|
||||||
|
|
||||||
|
// Only the response code for the first error is actually sent from the server
|
||||||
|
// this may be customized by the specific application
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature="ssr")]{
|
||||||
|
let response = use_context::<ResponseOptions>(cx);
|
||||||
|
if let Some(response) = response{
|
||||||
|
response.set_status(errors[0].status_code());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view! {cx,
|
||||||
|
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
|
||||||
|
<For
|
||||||
|
// a function that returns the items we're iterating over; a signal is fine
|
||||||
|
each= move || {errors.clone().into_iter().enumerate()}
|
||||||
|
// a unique key for each item as a reference
|
||||||
|
key=|(index, _error)| index.clone()
|
||||||
|
// renders each item to a view
|
||||||
|
view= move |error| {
|
||||||
|
let error_string = error.1.to_string();
|
||||||
|
let error_code= error.1.status_code();
|
||||||
|
view! {
|
||||||
|
cx,
|
||||||
|
<h2>{error_code.to_string()}</h2>
|
||||||
|
<p>"Error: " {error_string}</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
19
examples/errors_axum/src/errors.rs
Normal file
19
examples/errors_axum/src/errors.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use http::status::StatusCode;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
pub enum AppError {
|
||||||
|
#[error("Not Found")]
|
||||||
|
NotFound,
|
||||||
|
#[error("Internal Server Error")]
|
||||||
|
InternalServerError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppError {
|
||||||
|
pub fn status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
AppError::NotFound => StatusCode::NOT_FOUND,
|
||||||
|
AppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
examples/errors_axum/src/fallback.rs
Normal file
50
examples/errors_axum/src/fallback.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "ssr")] {
|
||||||
|
use axum::{
|
||||||
|
body::{boxed, Body, BoxBody},
|
||||||
|
extract::Extension,
|
||||||
|
response::IntoResponse,
|
||||||
|
http::{Request, Response, StatusCode, Uri},
|
||||||
|
};
|
||||||
|
use axum::response::Response as AxumResponse;
|
||||||
|
use tower::ServiceExt;
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use leptos::{LeptosOptions, Errors, view};
|
||||||
|
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
|
||||||
|
use crate::errors::AppError;
|
||||||
|
|
||||||
|
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||||
|
let options = &*options;
|
||||||
|
let root = options.site_root.clone();
|
||||||
|
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||||
|
|
||||||
|
if res.status() == StatusCode::OK {
|
||||||
|
res.into_response()
|
||||||
|
} else{
|
||||||
|
let mut errors = Errors::default();
|
||||||
|
errors.insert_with_default_key(AppError::NotFound);
|
||||||
|
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move |cx| view!{cx, <ErrorTemplate outside_errors=errors.clone()/>});
|
||||||
|
handler(req).await.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||||
|
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||||
|
let root_path = format!("{root}");
|
||||||
|
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||||
|
// This path is relative to the cargo root
|
||||||
|
match ServeDir::new(&root_path).oneshot(req).await {
|
||||||
|
Ok(res) => Ok(res.map(boxed)),
|
||||||
|
Err(err) => Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Something went wrong: {}", err),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
80
examples/errors_axum/src/landing.rs
Normal file
80
examples/errors_axum/src/landing.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
use crate::{
|
||||||
|
error_template::{ErrorTemplate, ErrorTemplateProps},
|
||||||
|
errors::AppError,
|
||||||
|
};
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_meta::*;
|
||||||
|
use leptos_router::*;
|
||||||
|
|
||||||
|
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||||
|
pub fn register_server_functions() {
|
||||||
|
_ = CauseInternalServerError::register();
|
||||||
|
_ = CauseNotFoundError::register();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
#[server(CauseInternalServerError, "/api")]
|
||||||
|
pub async fn cause_internal_server_error() -> Result<(), ServerFnError> {
|
||||||
|
// fake API delay
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(1250));
|
||||||
|
|
||||||
|
Err(ServerFnError::ServerError(
|
||||||
|
"Generic Server Error".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(CauseNotFoundError, "/api")]
|
||||||
|
pub async fn cause_not_found_error() -> Result<(), ServerFnError> {
|
||||||
|
Err(ServerFnError::ServerError("Not Found".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn App(cx: Scope) -> impl IntoView {
|
||||||
|
//let id = use_context::<String>(cx);
|
||||||
|
provide_meta_context(cx);
|
||||||
|
view! {
|
||||||
|
cx,
|
||||||
|
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||||
|
<Stylesheet id="leptos" href="/pkg/errors_axum.css"/>
|
||||||
|
<Router>
|
||||||
|
<header>
|
||||||
|
<h1>"Error Examples:"</h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<Routes>
|
||||||
|
<Route path="" view=|cx| view! {
|
||||||
|
cx,
|
||||||
|
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||||
|
<ExampleErrors/>
|
||||||
|
</ErrorBoundary>
|
||||||
|
}/>
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</Router>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ExampleErrors(cx: Scope) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
cx,
|
||||||
|
<p>
|
||||||
|
"This link will load a 404 page since it does not exist. Verify with browser development tools:"
|
||||||
|
<a href="/404">"This Page Does not Exist"</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
"The following <div> will always contain an error and cause the page to produce status 500. Check browser dev tools. "
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||||
|
<ReturnsError/>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ReturnsError(_cx: Scope) -> impl IntoView {
|
||||||
|
Err::<String, AppError>(AppError::InternalServerError)
|
||||||
|
}
|
25
examples/errors_axum/src/lib.rs
Normal file
25
examples/errors_axum/src/lib.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
use leptos::*;
|
||||||
|
pub mod error_template;
|
||||||
|
pub mod errors;
|
||||||
|
pub mod fallback;
|
||||||
|
pub mod landing;
|
||||||
|
|
||||||
|
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "hydrate")] {
|
||||||
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
use crate::landing::*;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn hydrate() {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
_ = console_log::init_with_level(log::Level::Debug);
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
leptos::mount_to_body(|cx| {
|
||||||
|
view! { cx, <App/> }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
74
examples/errors_axum/src/main.rs
Normal file
74
examples/errors_axum/src/main.rs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
use leptos::*;
|
||||||
|
// boilerplate to run in different modes
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "ssr")] {
|
||||||
|
use axum::{
|
||||||
|
routing::{post, get},
|
||||||
|
extract::{Extension, Path},
|
||||||
|
http::Request,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use axum::body::Body as AxumBody;
|
||||||
|
use crate::landing::*;
|
||||||
|
use errors_axum::*;
|
||||||
|
use crate::fallback::file_and_error_handler;
|
||||||
|
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
//Define a handler to test extractor with state
|
||||||
|
async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
|
||||||
|
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
|
||||||
|
move |cx| {
|
||||||
|
provide_context(cx, id.clone());
|
||||||
|
},
|
||||||
|
|cx| view! { cx, <App/> }
|
||||||
|
);
|
||||||
|
handler(req).await.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||||
|
|
||||||
|
crate::landing::register_server_functions();
|
||||||
|
|
||||||
|
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||||
|
let conf = get_configuration(None).await.unwrap();
|
||||||
|
let leptos_options = conf.leptos_options;
|
||||||
|
let addr = leptos_options.site_address.clone();
|
||||||
|
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||||
|
|
||||||
|
// build our application with a route
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||||
|
.route("/special/:id", get(custom_handler))
|
||||||
|
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
|
||||||
|
.fallback(file_and_error_handler)
|
||||||
|
.layer(Extension(Arc::new(leptos_options)));
|
||||||
|
|
||||||
|
// run our app with hyper
|
||||||
|
// `axum::Server` is a re-export of `hyper::Server`
|
||||||
|
log!("listening on http://{}", &addr);
|
||||||
|
axum::Server::bind(&addr)
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// client-only stuff for Trunk
|
||||||
|
else {
|
||||||
|
use todo_app_sqlite_axum::landing::*;
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
_ = console_log::init_with_level(log::Level::Debug);
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
mount_to_body(|cx| {
|
||||||
|
view! { cx, <App/> }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
examples/errors_axum/style.css
Normal file
3
examples/errors_axum/style.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.pending {
|
||||||
|
color: purple;
|
||||||
|
}
|
|
@ -34,12 +34,14 @@ sqlx = { version = "0.6.2", features = [
|
||||||
"runtime-tokio-rustls",
|
"runtime-tokio-rustls",
|
||||||
"sqlite",
|
"sqlite",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
thiserror = "1.0.38"
|
||||||
|
tracing = "0.1.37"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["csr"]
|
default = ["csr"]
|
||||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||||
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "dep:sqlx", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "leptos_axum"]
|
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "dep:sqlx", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_axum"]
|
||||||
|
|
||||||
[package.metadata.cargo-all-features]
|
[package.metadata.cargo-all-features]
|
||||||
denylist = [
|
denylist = [
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
|
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
|
||||||
|
|
||||||
## Client Side Rendering
|
## Client Side Rendering
|
||||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
This example cannot be built as a trunk standalone CSR-only app. Only the server may directly connect to the database.
|
||||||
app into one CSR bundle. Make sure you have trunk installed with `cargo install trunk`.
|
|
||||||
|
|
||||||
## Server Side Rendering with cargo-leptos
|
## Server Side Rendering with cargo-leptos
|
||||||
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
|
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
|
||||||
|
|
|
@ -1,28 +1,70 @@
|
||||||
|
use crate::errors::TodoAppError;
|
||||||
|
use cfg_if::cfg_if;
|
||||||
use leptos::Errors;
|
use leptos::Errors;
|
||||||
use leptos::{view, For, ForProps, IntoView, RwSignal, Scope, View};
|
use leptos::{
|
||||||
|
component, create_rw_signal, use_context, view, For, ForProps, IntoView, RwSignal, Scope,
|
||||||
|
};
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use leptos_axum::ResponseOptions;
|
||||||
|
|
||||||
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
|
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
|
||||||
// here than just displaying them
|
// here than just displaying them
|
||||||
pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
|
#[component]
|
||||||
let Some(errors) = errors else {
|
pub fn ErrorTemplate(
|
||||||
panic!("No Errors found and we expected errors!");
|
cx: Scope,
|
||||||
|
#[prop(optional)] outside_errors: Option<Errors>,
|
||||||
|
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let errors = match outside_errors {
|
||||||
|
Some(e) => {
|
||||||
|
let errors = create_rw_signal(cx, e);
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
None => match errors {
|
||||||
|
Some(e) => e,
|
||||||
|
None => panic!("No Errors found and we expected errors!"),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get Errors from Signal
|
||||||
|
let errors = errors.get().0;
|
||||||
|
|
||||||
|
// Downcast lets us take a type that implements `std::error::Error`
|
||||||
|
let errors: Vec<TodoAppError> = errors
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned())
|
||||||
|
.flatten()
|
||||||
|
.collect();
|
||||||
|
println!("Errors: {errors:#?}");
|
||||||
|
|
||||||
|
// Only the response code for the first error is actually sent from the server
|
||||||
|
// this may be customized by the specific application
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature="ssr")]{
|
||||||
|
let response = use_context::<ResponseOptions>(cx);
|
||||||
|
if let Some(response) = response{
|
||||||
|
response.set_status(errors[0].status_code());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
view! {cx,
|
view! {cx,
|
||||||
<h1>"Errors"</h1>
|
<h1>"Errors"</h1>
|
||||||
<For
|
<For
|
||||||
// a function that returns the items we're iterating over; a signal is fine
|
// a function that returns the items we're iterating over; a signal is fine
|
||||||
each= move || {errors.get().0.into_iter()}
|
each= move || {errors.clone().into_iter().enumerate()}
|
||||||
// a unique key for each item as a reference
|
// a unique key for each item as a reference
|
||||||
key=|error| error.0.clone()
|
key=|(index, _error)| index.clone()
|
||||||
// renders each item to a view
|
// renders each item to a view
|
||||||
view= move |error| {
|
view= move |error| {
|
||||||
let error_string = error.1.to_string();
|
let error_string = error.1.to_string();
|
||||||
|
let error_code= error.1.status_code();
|
||||||
view! {
|
view! {
|
||||||
cx,
|
cx,
|
||||||
|
<h2>{error_code.to_string()}</h2>
|
||||||
<p>"Error: " {error_string}</p>
|
<p>"Error: " {error_string}</p>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
.into_view(cx)
|
|
||||||
}
|
}
|
||||||
|
|
19
examples/todo_app_sqlite_axum/src/errors.rs
Normal file
19
examples/todo_app_sqlite_axum/src/errors.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use http::status::StatusCode;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
pub enum TodoAppError {
|
||||||
|
#[error("Not Found")]
|
||||||
|
NotFound,
|
||||||
|
#[error("Internal Server Error")]
|
||||||
|
InternalServerError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TodoAppError {
|
||||||
|
pub fn status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
TodoAppError::NotFound => StatusCode::NOT_FOUND,
|
||||||
|
TodoAppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,8 +12,9 @@ if #[cfg(feature = "ssr")] {
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use leptos::{LeptosOptions};
|
use leptos::{LeptosOptions, Errors, view};
|
||||||
use crate::error_template::error_template;
|
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
|
||||||
|
use crate::errors::TodoAppError;
|
||||||
|
|
||||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||||
let options = &*options;
|
let options = &*options;
|
||||||
|
@ -23,7 +24,9 @@ if #[cfg(feature = "ssr")] {
|
||||||
if res.status() == StatusCode::OK {
|
if res.status() == StatusCode::OK {
|
||||||
res.into_response()
|
res.into_response()
|
||||||
} else{
|
} else{
|
||||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
|
let mut errors = Errors::default();
|
||||||
|
errors.insert_with_default_key(TodoAppError::NotFound);
|
||||||
|
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move |cx| view!{cx, <ErrorTemplate outside_errors=errors.clone()/>});
|
||||||
handler(req).await.into_response()
|
handler(req).await.into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use cfg_if::cfg_if;
|
use cfg_if::cfg_if;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
pub mod error_template;
|
pub mod error_template;
|
||||||
|
pub mod errors;
|
||||||
pub mod fallback;
|
pub mod fallback;
|
||||||
pub mod todo;
|
pub mod todo;
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ if #[cfg(feature = "ssr")] {
|
||||||
routing::{post, get},
|
routing::{post, get},
|
||||||
extract::{Extension, Path},
|
extract::{Extension, Path},
|
||||||
http::Request,
|
http::Request,
|
||||||
body::StreamBody,
|
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
@ -17,7 +16,6 @@ if #[cfg(feature = "ssr")] {
|
||||||
use crate::fallback::file_and_error_handler;
|
use crate::fallback::file_and_error_handler;
|
||||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use leptos_reactive::run_scope;
|
|
||||||
|
|
||||||
//Define a handler to test extractor with state
|
//Define a handler to test extractor with state
|
||||||
async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
|
async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
|
||||||
use cfg_if::cfg_if;
|
use cfg_if::cfg_if;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_meta::*;
|
use leptos_meta::*;
|
||||||
|
@ -41,8 +42,8 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||||
// http::Request doesn't implement Clone, so more work will be needed to do use_context() on this
|
// http::Request doesn't implement Clone, so more work will be needed to do use_context() on this
|
||||||
let req_parts = use_context::<leptos_axum::RequestParts>(cx);
|
let req_parts = use_context::<leptos_axum::RequestParts>(cx);
|
||||||
|
|
||||||
if let Some(req_parts) = req_parts{
|
if let Some(req_parts) = req_parts {
|
||||||
println!("Uri = {:?}", req_parts.uri);
|
println!("Uri = {:?}", req_parts.uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
@ -107,7 +108,7 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||||
let id = use_context::<String>(cx);
|
//let id = use_context::<String>(cx);
|
||||||
provide_meta_context(cx);
|
provide_meta_context(cx);
|
||||||
view! {
|
view! {
|
||||||
cx,
|
cx,
|
||||||
|
@ -121,8 +122,10 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="" view=|cx| view! {
|
<Route path="" view=|cx| view! {
|
||||||
cx,
|
cx,
|
||||||
|
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||||
<Todos/>
|
<Todos/>
|
||||||
}/>
|
</ErrorBoundary>
|
||||||
|
}/> //Route
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
|
@ -35,7 +35,7 @@ pub fn ErrorBoundary<F, IV>(
|
||||||
fallback: F,
|
fallback: F,
|
||||||
) -> impl IntoView
|
) -> impl IntoView
|
||||||
where
|
where
|
||||||
F: Fn(Scope, Option<RwSignal<Errors>>) -> IV + 'static,
|
F: Fn(Scope, RwSignal<Errors>) -> IV + 'static,
|
||||||
IV: IntoView,
|
IV: IntoView,
|
||||||
{
|
{
|
||||||
let errors: RwSignal<Errors> = create_rw_signal(cx, Errors::default());
|
let errors: RwSignal<Errors> = create_rw_signal(cx, Errors::default());
|
||||||
|
@ -47,6 +47,6 @@ where
|
||||||
|
|
||||||
move || match errors.get().0.is_empty() {
|
move || match errors.get().0.is_empty() {
|
||||||
true => children.clone().into_view(cx),
|
true => children.clone().into_view(cx),
|
||||||
false => fallback(cx, Some(errors)).into_view(cx),
|
false => fallback(cx, errors).into_view(cx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,12 @@ use std::{collections::HashMap, error::Error, sync::Arc};
|
||||||
|
|
||||||
/// A struct to hold all the possible errors that could be provided by child Views
|
/// A struct to hold all the possible errors that could be provided by child Views
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct Errors(pub HashMap<HydrationKey, Arc<dyn Error>>);
|
pub struct Errors(pub HashMap<HydrationKey, Arc<dyn Error + Send + Sync>>);
|
||||||
|
|
||||||
impl<T, E> IntoView for Result<T, E>
|
impl<T, E> IntoView for Result<T, E>
|
||||||
where
|
where
|
||||||
T: IntoView + 'static,
|
T: IntoView + 'static,
|
||||||
E: std::error::Error + Send + Sync + 'static,
|
E: Error + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
|
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
|
||||||
match self {
|
match self {
|
||||||
|
@ -59,14 +59,21 @@ impl Errors {
|
||||||
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
|
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||||
pub fn insert<E>(&mut self, key: HydrationKey, error: E)
|
pub fn insert<E>(&mut self, key: HydrationKey, error: E)
|
||||||
where
|
where
|
||||||
E: Error + 'static,
|
E: Error + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
self.0.insert(key, Arc::new(error));
|
self.0.insert(key, Arc::new(error));
|
||||||
}
|
}
|
||||||
|
/// Add an error with the default key for errors outside the reactive system
|
||||||
|
pub fn insert_with_default_key<E>(&mut self, error: E)
|
||||||
|
where
|
||||||
|
E: Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.0.insert(HydrationKey::default(), Arc::new(error));
|
||||||
|
}
|
||||||
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
|
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||||
pub fn remove<E>(&mut self, key: &HydrationKey)
|
pub fn remove<E>(&mut self, key: &HydrationKey)
|
||||||
where
|
where
|
||||||
E: Error + 'static,
|
E: Error + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
self.0.remove(key);
|
self.0.remove(key);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue