redirects, middleware, Transport, set/capture error status (#4751)
* wip: redirect * redirect example * fix file upload not in a form * wip * deny missing cfgs * wip: add router fallback but it's not working * fix suspense bug after rebuilds that queued work * more cleanup * add test for error codes * add helpful comment * bring back middleware annotation * fix cfg * add server feature * fix doctest * just disable liveview test for now * fix liveview * dont send init * cache more playwright! * add more pkgs to cache
This commit is contained in:
1 parent
8a81475ba8
commit
9243ef4128
35 files changed
+899
-764
No files matched your search
@@ -234,7 +234,7 @@ jobs:
|
|||||||
uses: ./.github/actions/free-disk-space
|
uses: ./.github/actions/free-disk-space
|
||||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||||
with:
|
with:
|
||||||
packages: glib-networking glib-networking-common glib-networking-services libaa1 libabsl20220623t64 libass9 libasyncns0 libavc1394-0 libavcodec60 libavfilter9 libavformat60 libavtp0 libavutil58 libblas3 libbluray2 libbs2b0 libcaca0 libcairo-script-interpreter2 libcdparanoia0 libchromaprint1 libcjson1 libcodec2-1.2 libdav1d7 libdc1394-25 libdca0 libdecor-0-0 libdirectfb-1.7-7t64 libdv4t64 libdvdnav4 libdvdread8t64 libfaad2 libflac12t64 libfluidsynth3 libfreeaptx0 libgav1-1 libgme0 libgraphene-1.0-0 libgsm1 libgssdp-1.6-0 libgstreamer-plugins-good1.0-0 libgtk-4-common libgupnp-1.6-0 libgupnp-igd-1.6-0 libhwy1t64 libiec61883-0 libimath-3-1-29t64 libinstpatch-1.0-2 libjack-jackd2-0 libjxl0.7 liblapack3 liblc3-1 libldacbt-enc2 liblilv-0-0 liblrdf0 libltc11 libmbedcrypto7t64 libmfx1 libmjpegutils-2.1-0t64 libmodplug1 libmp3lame0 libmpcdec6 libmpeg2encpp-2.1-0t64 libmpg123-0t64 libmplex2-2.1-0t64 libmysofa1 libneon27t64 libnice10 libogg0 libopenal-data libopenal1 libopenexr-3-1-30 libopenh264-7 libopenmpt0t64 libopenni2-0 liborc-0.4-0t64 libpipewire-0.3-0t64 libplacebo338 libpocketsphinx3 libpostproc57 libproxy1v5 libpulse0 libqrencode4 libraptor2-0 librav1e0 libraw1394-11 librist4 librubberband2 libsamplerate0 libsbc1 libsdl2-2.0-0 libsecret-common libserd-0-0 libshine3 libshout3 libsndfile1 libsndio7.0 libsord-0-0 libsoundtouch1 libsoup-3.0-0 libsoup-3.0-common libsoxr0 libspa-0.2-modules libspandsp2t64 libspeex1 libsphinxbase3t64 libsratom-0-0 libsrt1.5-gnutls libsrtp2-1 libssh-gcrypt-4 libsvtav1enc1d1 libswresample4 libswscale7 libtag1v5 libtag1v5-vanilla libtheora0 libtwolame0 libudfread0 libunibreak5 libv4l-0t64 libv4lconvert0t64 libva-drm2 libva-x11-2 libva2 libvdpau1 libvidstab1.1 libvisual-0.4-0 libvo-aacenc0 libvo-amrwbenc0 libvorbis0a libvorbisenc2 libvorbisfile3 libvpl2 libwavpack1 libwebrtc-audio-processing1 libwildmidi2 libx265-199 libxcb-xkb1 libxkbcommon-x11-0 libxml2-dev libxvidcore4 libyuv0 libzbar0t64 libzimg2 libzix-0-0 libzvbi-common libzvbi0t64 libzxing3 ocl-icd-libopencl1 timgm6mb-soundfont xfonts-encodings xfonts-utils binutils lld binutils-devel binutils-gold
|
packages: glib-networking glib-networking-common glib-networking-services libaa1 libabsl20220623t64 libass9 libasyncns0 libavc1394-0 libavcodec60 libavfilter9 libavformat60 libavtp0 libavutil58 libblas3 libbluray2 libbs2b0 libcaca0 libcairo-script-interpreter2 libcdparanoia0 libchromaprint1 libcjson1 libcodec2-1.2 libdav1d7 libdc1394-25 libdca0 libdecor-0-0 libdirectfb-1.7-7t64 libdv4t64 libdvdnav4 libdvdread8t64 libfaad2 libflac12t64 libfluidsynth3 libfreeaptx0 libgav1-1 libgme0 libgraphene-1.0-0 libgsm1 libgssdp-1.6-0 libgstreamer-plugins-good1.0-0 libgtk-4-common libgupnp-1.6-0 libgupnp-igd-1.6-0 libhwy1t64 libiec61883-0 libimath-3-1-29t64 libinstpatch-1.0-2 libjack-jackd2-0 libjxl0.7 liblapack3 liblc3-1 libldacbt-enc2 liblilv-0-0 liblrdf0 libltc11 libmbedcrypto7t64 libmfx1 libmjpegutils-2.1-0t64 libmodplug1 libmp3lame0 libmpcdec6 libmpeg2encpp-2.1-0t64 libmpg123-0t64 libmplex2-2.1-0t64 libmysofa1 libneon27t64 libnice10 libogg0 libopenal-data libopenal1 libopenexr-3-1-30 libopenh264-7 libopenmpt0t64 libopenni2-0 liborc-0.4-0t64 libpipewire-0.3-0t64 libplacebo338 libpocketsphinx3 libpostproc57 libproxy1v5 libpulse0 libqrencode4 libraptor2-0 librav1e0 libraw1394-11 librist4 librubberband2 libsamplerate0 libsbc1 libsdl2-2.0-0 libsecret-common libserd-0-0 libshine3 libshout3 libsndfile1 libsndio7.0 libsord-0-0 libsoundtouch1 libsoup-3.0-0 libsoup-3.0-common libsoxr0 libspa-0.2-modules libspandsp2t64 libspeex1 libsphinxbase3t64 libsratom-0-0 libsrt1.5-gnutls libsrtp2-1 libssh-gcrypt-4 libsvtav1enc1d1 libswresample4 libswscale7 libtag1v5 libtag1v5-vanilla libtheora0 libtwolame0 libudfread0 libunibreak5 libv4l-0t64 libv4lconvert0t64 libva-drm2 libva-x11-2 libva2 libvdpau1 libvidstab1.1 libvisual-0.4-0 libvo-aacenc0 libvo-amrwbenc0 libvorbis0a libvorbisenc2 libvorbisfile3 libvpl2 libwavpack1 libwebrtc-audio-processing1 libwildmidi2 libx265-199 libxcb-xkb1 libxkbcommon-x11-0 libcups2t64 libxml2 libxml2-dev libxvidcore4 libyuv0 libzbar0t64 libzimg2 libzix-0-0 libzvbi-common libzvbi0t64 libzxing3 ocl-icd-libopencl1 timgm6mb-soundfont xfonts-encodings xfonts-utils binutils lld binutils-devel binutils-gold fonts-freefont-ttf fonts-ipafont-gothic fonts-tlwg-loma-otf fonts-unifont fonts-wqy-zenhei gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-plugins-good libavif16 libevent-2.1-7t64 libgstreamer-plugins-bad1.0-0 libharfbuzz-icu0 libhyphen0 libmanette-0.2-0 libsecret-1-0 libwoff1 xfonts-cyrillic xfonts-scalable fonts-ipafont-mincho fonts-tlwg-loma gstreamer1.0-x
|
||||||
version: 1.0
|
version: 1.0
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
Generated
+8
@@ -5705,6 +5705,7 @@ dependencies = [
|
|||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower-http",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wasm-splitter",
|
"wasm-splitter",
|
||||||
"wasm-streams",
|
"wasm-streams",
|
||||||
@@ -6010,6 +6011,13 @@ dependencies = [
|
|||||||
"dioxus",
|
"dioxus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dioxus-playwright-fullstack-error-codes-test"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"dioxus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dioxus-playwright-fullstack-errors-test"
|
name = "dioxus-playwright-fullstack-errors-test"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
+17
@@ -137,6 +137,7 @@ members = [
|
|||||||
"packages/playwright-tests/cli-optimization",
|
"packages/playwright-tests/cli-optimization",
|
||||||
"packages/playwright-tests/wasm-split-harness",
|
"packages/playwright-tests/wasm-split-harness",
|
||||||
"packages/playwright-tests/default-features-disabled",
|
"packages/playwright-tests/default-features-disabled",
|
||||||
|
"packages/playwright-tests/fullstack-error-codes",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
@@ -478,6 +479,7 @@ bytes = { workspace = true }
|
|||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
axum-core = { workspace = true }
|
axum-core = { workspace = true }
|
||||||
uuid = { workspace = true, features = ["v4", "serde"] }
|
uuid = { workspace = true, features = ["v4", "serde"] }
|
||||||
|
tower-http = { workspace = true, features = ["timeout"] }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||||
getrandom = { workspace = true, features = ["wasm_js"] }
|
getrandom = { workspace = true, features = ["wasm_js"] }
|
||||||
@@ -697,6 +699,21 @@ name = "flat_router"
|
|||||||
path = "examples/06-routing/flat_router.rs"
|
path = "examples/06-routing/flat_router.rs"
|
||||||
doc-scrape-examples = true
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "middleware"
|
||||||
|
path = "examples/07-fullstack/middleware.rs"
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "custom_error_page"
|
||||||
|
path = "examples/07-fullstack/custom_error_page.rs"
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "redirect"
|
||||||
|
path = "examples/07-fullstack/redirect.rs"
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "header_map"
|
name = "header_map"
|
||||||
path = "examples/07-fullstack/header_map.rs"
|
path = "examples/07-fullstack/header_map.rs"
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
//! To render custom error pages, you can create a layout component that captures errors from routes
|
||||||
|
//! with an `ErrorBoundary` and display different content based on the error type.
|
||||||
|
//!
|
||||||
|
//! While capturing the error, we set the appropriate HTTP status code using `FullstackContext::commit_error_status`.
|
||||||
|
//! The router will then use this status code when doing server-side rendering (SSR).
|
||||||
|
//!
|
||||||
|
//! Any errors not captured by an error boundary will be handled by dioxus-ssr itself, which will render
|
||||||
|
//! a generic error page instead.
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_fullstack::{FullstackContext, StatusCode};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus::launch(|| {
|
||||||
|
rsx! {
|
||||||
|
Router::<Route> {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Routable, PartialEq, Clone, Debug)]
|
||||||
|
enum Route {
|
||||||
|
#[layout(ErrorLayout)]
|
||||||
|
#[route("/")]
|
||||||
|
Home,
|
||||||
|
|
||||||
|
#[route("/blog/:id")]
|
||||||
|
Blog { id: u32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Home() -> Element {
|
||||||
|
rsx! {
|
||||||
|
div { "Welcome to the home page!" }
|
||||||
|
div { display: "flex", flex_direction: "column",
|
||||||
|
Link { to: Route::Blog { id: 1 }, "Go to blog post 1" }
|
||||||
|
Link { to: Route::Blog { id: 2 }, "Go to blog post 2" }
|
||||||
|
Link { to: Route::Blog { id: 3 }, "Go to blog post 3 (error)" }
|
||||||
|
Link { to: Route::Blog { id: 4 }, "Go to blog post 4 (not found)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Blog(id: u32) -> Element {
|
||||||
|
match id {
|
||||||
|
1 => rsx! { div { "Blog post 1" } },
|
||||||
|
2 => rsx! { div { "Blog post 2" } },
|
||||||
|
3 => dioxus_core::bail!("An error occurred while loading the blog post!"),
|
||||||
|
_ => HttpError::not_found("Blog post not found")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// In our `ErrorLayout` component, we wrap the `Outlet` in an `ErrorBoundary`. This lets us attempt
|
||||||
|
/// to downcast the error to an `HttpError` and set the appropriate status code.
|
||||||
|
///
|
||||||
|
/// The `commit_error_status` function will attempt to downcast the error to an `HttpError` and
|
||||||
|
/// set the status code accordingly. Note that you can commit any status code you want with `commit_http_status`.
|
||||||
|
///
|
||||||
|
/// The router will automatically set the HTTP status code when doing SSR.
|
||||||
|
#[component]
|
||||||
|
fn ErrorLayout() -> Element {
|
||||||
|
rsx! {
|
||||||
|
ErrorBoundary {
|
||||||
|
handle_error: move |err: ErrorContext| {
|
||||||
|
let http_error = FullstackContext::commit_error_status(err.error().unwrap());
|
||||||
|
match http_error.status {
|
||||||
|
StatusCode::NOT_FOUND => rsx! { div { "404 - Page not found" } },
|
||||||
|
StatusCode::UNAUTHORIZED => rsx! { div { "401 - Unauthorized" } },
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR => rsx! { div { "500 - Internal Server Error" } },
|
||||||
|
_ => rsx! { div { "An unknown error occurred" } },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Outlet::<Route> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
//! This example shows how to use middleware in a fullstack Dioxus app.
|
||||||
|
//!
|
||||||
|
//! Dioxus supports two ways of middleware:
|
||||||
|
//! - Applying layers to the top-level axum router
|
||||||
|
//! - Apply `#[middleware]` attributes to individual handlers
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
use {std::time::Duration, tower_http::timeout::TimeoutLayer};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
#[cfg(not(feature = "server"))]
|
||||||
|
dioxus::launch(app);
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
dioxus::serve(|| async move {
|
||||||
|
use axum::{extract::Request, middleware::Next};
|
||||||
|
use dioxus::server::axum;
|
||||||
|
|
||||||
|
Ok(dioxus::server::router(app)
|
||||||
|
// we can apply a layer to the entire router using axum's `.layer` method
|
||||||
|
.layer(axum::middleware::from_fn(
|
||||||
|
|request: Request, next: Next| async move {
|
||||||
|
println!("Request: {} {}", request.method(), request.uri().path());
|
||||||
|
let res = next.run(request).await;
|
||||||
|
println!("Response: {}", res.status());
|
||||||
|
res
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app() -> Element {
|
||||||
|
let mut per_route = use_action(per_route_middleware);
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
h1 { "Fullstack Middleware Example" }
|
||||||
|
button { onclick: move |_| per_route.call(), "Fetch Data" }
|
||||||
|
pre { "{per_route.result():#?}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can use the `#[middleware]` attribute to apply middleware to individual handlers.
|
||||||
|
//
|
||||||
|
// Here, we're applying a timeout to the `per_route_middleware` handler, which will return a 504
|
||||||
|
// if the handler takes longer than 3 seconds to complete.
|
||||||
|
//
|
||||||
|
// To add multiple middleware layers, simply stack multiple `#[middleware]` attributes.
|
||||||
|
#[get("/api/count")]
|
||||||
|
#[middleware(TimeoutLayer::new(Duration::from_secs(3)))]
|
||||||
|
async fn per_route_middleware() -> Result<String> {
|
||||||
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||||
|
Ok("Hello, world!".to_string())
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
//! This example shows how to use the axum `Redirect` type to redirect the client to a different URL.
|
||||||
|
//!
|
||||||
|
//! On the web, a redirect will not be handled directly by JS, but instead the browser will automatically
|
||||||
|
//! follow the redirect. This is useful for redirecting to different pages after a form submission.
|
||||||
|
//!
|
||||||
|
//! Note that redirects returned to the client won't navigate the SPA to a new page automatically.
|
||||||
|
//! For managing a session or auth with client side routing, you'll need to handle that in the SPA itself.
|
||||||
|
|
||||||
|
use dioxus::{fullstack::Redirect, prelude::*};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus::launch(|| {
|
||||||
|
rsx! {
|
||||||
|
Router::<Route> {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Routable)]
|
||||||
|
enum Route {
|
||||||
|
#[route("/")]
|
||||||
|
Home,
|
||||||
|
|
||||||
|
#[route("/blog")]
|
||||||
|
Blog,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Home() -> Element {
|
||||||
|
rsx! {
|
||||||
|
h1 { "Welcome home" }
|
||||||
|
form {
|
||||||
|
method: "post",
|
||||||
|
action: "/api/old-blog",
|
||||||
|
button { "Go to blog" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Blog() -> Element {
|
||||||
|
rsx! {
|
||||||
|
h1 { "Welcome to the blog!" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/api/old-blog")]
|
||||||
|
async fn redirect_to_blog() -> Result<Redirect> {
|
||||||
|
Ok(Redirect::to("/blog"))
|
||||||
|
}
|
||||||
+126
-125
@@ -201,131 +201,6 @@ pub struct ErrorBoundaryProps {
|
|||||||
children: Element,
|
children: Element,
|
||||||
handle_error: ErrorHandler,
|
handle_error: ErrorHandler,
|
||||||
}
|
}
|
||||||
impl ErrorBoundaryProps {
|
|
||||||
/**
|
|
||||||
Create a builder for building `ErrorBoundaryProps`.
|
|
||||||
On the builder, call `.children(...)`(optional), `.handle_error(...)`(optional) to set the values of the fields.
|
|
||||||
Finally, call `.build()` to create the instance of `ErrorBoundaryProps`.
|
|
||||||
*/
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn builder() -> ErrorBoundaryPropsBuilder<((), ())> {
|
|
||||||
ErrorBoundaryPropsBuilder { fields: ((), ()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
|
||||||
pub struct ErrorBoundaryPropsBuilder<TypedBuilderFields> {
|
|
||||||
fields: TypedBuilderFields,
|
|
||||||
}
|
|
||||||
impl<TypedBuilderFields> Clone for ErrorBoundaryPropsBuilder<TypedBuilderFields>
|
|
||||||
where
|
|
||||||
TypedBuilderFields: Clone,
|
|
||||||
{
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
fields: self.fields.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Properties for ErrorBoundaryProps {
|
|
||||||
type Builder = ErrorBoundaryPropsBuilder<((), ())>;
|
|
||||||
fn builder() -> Self::Builder {
|
|
||||||
ErrorBoundaryProps::builder()
|
|
||||||
}
|
|
||||||
fn memoize(&mut self, other: &Self) -> bool {
|
|
||||||
*self = other.clone();
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
|
||||||
pub trait ErrorBoundaryPropsBuilder_Optional<T> {
|
|
||||||
fn into_value<F: FnOnce() -> T>(self, default: F) -> T;
|
|
||||||
}
|
|
||||||
impl<T> ErrorBoundaryPropsBuilder_Optional<T> for () {
|
|
||||||
fn into_value<F: FnOnce() -> T>(self, default: F) -> T {
|
|
||||||
default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> ErrorBoundaryPropsBuilder_Optional<T> for (T,) {
|
|
||||||
fn into_value<F: FnOnce() -> T>(self, _: F) -> T {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
|
||||||
impl<__handle_error> ErrorBoundaryPropsBuilder<((), __handle_error)> {
|
|
||||||
pub fn children(
|
|
||||||
self,
|
|
||||||
children: Element,
|
|
||||||
) -> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
|
|
||||||
let children = (children,);
|
|
||||||
let (_, handle_error) = self.fields;
|
|
||||||
ErrorBoundaryPropsBuilder {
|
|
||||||
fields: (children, handle_error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
|
||||||
pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_children {}
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
|
||||||
impl<__handle_error> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
|
|
||||||
#[deprecated(note = "Repeated field children")]
|
|
||||||
pub fn children(
|
|
||||||
self,
|
|
||||||
_: ErrorBoundaryPropsBuilder_Error_Repeated_field_children,
|
|
||||||
) -> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
|
||||||
impl<__children> ErrorBoundaryPropsBuilder<(__children, ())> {
|
|
||||||
pub fn handle_error(
|
|
||||||
self,
|
|
||||||
handle_error: impl ::core::convert::Into<ErrorHandler>,
|
|
||||||
) -> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
|
|
||||||
let handle_error = (handle_error.into(),);
|
|
||||||
let (children, _) = self.fields;
|
|
||||||
ErrorBoundaryPropsBuilder {
|
|
||||||
fields: (children, handle_error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
|
||||||
pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error {}
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
|
||||||
impl<__children> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
|
|
||||||
#[deprecated(note = "Repeated field handle_error")]
|
|
||||||
pub fn handle_error(
|
|
||||||
self,
|
|
||||||
_: ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error,
|
|
||||||
) -> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
|
||||||
impl<
|
|
||||||
__handle_error: ErrorBoundaryPropsBuilder_Optional<ErrorHandler>,
|
|
||||||
__children: ErrorBoundaryPropsBuilder_Optional<Element>,
|
|
||||||
> ErrorBoundaryPropsBuilder<(__children, __handle_error)>
|
|
||||||
{
|
|
||||||
pub fn build(self) -> ErrorBoundaryProps {
|
|
||||||
let (children, handle_error) = self.fields;
|
|
||||||
let children = ErrorBoundaryPropsBuilder_Optional::into_value(children, VNode::empty);
|
|
||||||
let handle_error = ErrorBoundaryPropsBuilder_Optional::into_value(handle_error, || {
|
|
||||||
ErrorHandler(Rc::new(default_handler))
|
|
||||||
});
|
|
||||||
ErrorBoundaryProps {
|
|
||||||
children,
|
|
||||||
handle_error,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new error boundary component that catches any errors thrown from child components
|
/// Create a new error boundary component that catches any errors thrown from child components
|
||||||
///
|
///
|
||||||
@@ -459,3 +334,129 @@ pub fn ErrorBoundary(props: ErrorBoundaryProps) -> Element {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ErrorBoundaryProps {
|
||||||
|
/**
|
||||||
|
Create a builder for building `ErrorBoundaryProps`.
|
||||||
|
On the builder, call `.children(...)`(optional), `.handle_error(...)`(optional) to set the values of the fields.
|
||||||
|
Finally, call `.build()` to create the instance of `ErrorBoundaryProps`.
|
||||||
|
*/
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn builder() -> ErrorBoundaryPropsBuilder<((), ())> {
|
||||||
|
ErrorBoundaryPropsBuilder { fields: ((), ()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||||
|
pub struct ErrorBoundaryPropsBuilder<TypedBuilderFields> {
|
||||||
|
fields: TypedBuilderFields,
|
||||||
|
}
|
||||||
|
impl<TypedBuilderFields> Clone for ErrorBoundaryPropsBuilder<TypedBuilderFields>
|
||||||
|
where
|
||||||
|
TypedBuilderFields: Clone,
|
||||||
|
{
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
fields: self.fields.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Properties for ErrorBoundaryProps {
|
||||||
|
type Builder = ErrorBoundaryPropsBuilder<((), ())>;
|
||||||
|
fn builder() -> Self::Builder {
|
||||||
|
ErrorBoundaryProps::builder()
|
||||||
|
}
|
||||||
|
fn memoize(&mut self, other: &Self) -> bool {
|
||||||
|
*self = other.clone();
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||||
|
pub trait ErrorBoundaryPropsBuilder_Optional<T> {
|
||||||
|
fn into_value<F: FnOnce() -> T>(self, default: F) -> T;
|
||||||
|
}
|
||||||
|
impl<T> ErrorBoundaryPropsBuilder_Optional<T> for () {
|
||||||
|
fn into_value<F: FnOnce() -> T>(self, default: F) -> T {
|
||||||
|
default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> ErrorBoundaryPropsBuilder_Optional<T> for (T,) {
|
||||||
|
fn into_value<F: FnOnce() -> T>(self, _: F) -> T {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
impl<__handle_error> ErrorBoundaryPropsBuilder<((), __handle_error)> {
|
||||||
|
pub fn children(
|
||||||
|
self,
|
||||||
|
children: Element,
|
||||||
|
) -> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
|
||||||
|
let children = (children,);
|
||||||
|
let (_, handle_error) = self.fields;
|
||||||
|
ErrorBoundaryPropsBuilder {
|
||||||
|
fields: (children, handle_error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||||
|
pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_children {}
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
impl<__handle_error> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
|
||||||
|
#[deprecated(note = "Repeated field children")]
|
||||||
|
pub fn children(
|
||||||
|
self,
|
||||||
|
_: ErrorBoundaryPropsBuilder_Error_Repeated_field_children,
|
||||||
|
) -> ErrorBoundaryPropsBuilder<((Element,), __handle_error)> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
impl<__children> ErrorBoundaryPropsBuilder<(__children, ())> {
|
||||||
|
pub fn handle_error(
|
||||||
|
self,
|
||||||
|
handle_error: impl ::core::convert::Into<ErrorHandler>,
|
||||||
|
) -> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
|
||||||
|
let handle_error = (handle_error.into(),);
|
||||||
|
let (children, _) = self.fields;
|
||||||
|
ErrorBoundaryPropsBuilder {
|
||||||
|
fields: (children, handle_error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||||
|
pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error {}
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
impl<__children> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
|
||||||
|
#[deprecated(note = "Repeated field handle_error")]
|
||||||
|
pub fn handle_error(
|
||||||
|
self,
|
||||||
|
_: ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error,
|
||||||
|
) -> ErrorBoundaryPropsBuilder<(__children, (ErrorHandler,))> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
impl<
|
||||||
|
__handle_error: ErrorBoundaryPropsBuilder_Optional<ErrorHandler>,
|
||||||
|
__children: ErrorBoundaryPropsBuilder_Optional<Element>,
|
||||||
|
> ErrorBoundaryPropsBuilder<(__children, __handle_error)>
|
||||||
|
{
|
||||||
|
pub fn build(self) -> ErrorBoundaryProps {
|
||||||
|
let (children, handle_error) = self.fields;
|
||||||
|
let children = ErrorBoundaryPropsBuilder_Optional::into_value(children, VNode::empty);
|
||||||
|
let handle_error = ErrorBoundaryPropsBuilder_Optional::into_value(handle_error, || {
|
||||||
|
ErrorHandler(Rc::new(default_handler))
|
||||||
|
});
|
||||||
|
ErrorBoundaryProps {
|
||||||
|
children,
|
||||||
|
handle_error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -115,7 +115,7 @@ impl<E: Into<Error>> From<E> for CapturedError {
|
|||||||
|
|
||||||
impl std::fmt::Display for CapturedError {
|
impl std::fmt::Display for CapturedError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}", self.0)
|
std::fmt::Display::fmt(&*self.0, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
any_props::{AnyProps, BoxedAnyProps},
|
any_props::{AnyProps, BoxedAnyProps},
|
||||||
innerlude::{throw_error, RenderError, ScopeOrder, ScopeState},
|
innerlude::{RenderError, ScopeOrder, ScopeState},
|
||||||
scope_context::{Scope, SuspenseLocation},
|
scope_context::{Scope, SuspenseLocation},
|
||||||
scopes::ScopeId,
|
scopes::ScopeId,
|
||||||
virtual_dom::VirtualDom,
|
virtual_dom::VirtualDom,
|
||||||
@@ -108,8 +108,8 @@ impl VirtualDom {
|
|||||||
fn handle_element_return(&self, node: &mut Element, scope: &Scope) {
|
fn handle_element_return(&self, node: &mut Element, scope: &Scope) {
|
||||||
match node {
|
match node {
|
||||||
Err(RenderError::Error(e)) => {
|
Err(RenderError::Error(e)) => {
|
||||||
tracing::error!("Error while rendering component `{}`:\n{e}", scope.name);
|
tracing::error!("Error while rendering component `{}`: {e}", scope.name);
|
||||||
throw_error(e.clone());
|
self.runtime.throw_error(scope.id, e.clone());
|
||||||
}
|
}
|
||||||
Err(RenderError::Suspended(e)) => {
|
Err(RenderError::Suspended(e)) => {
|
||||||
let task = e.task();
|
let task = e.task();
|
||||||
|
|||||||
@@ -640,7 +640,7 @@ impl VirtualDom {
|
|||||||
#[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_suspense")]
|
#[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_suspense")]
|
||||||
pub async fn wait_for_suspense(&mut self) {
|
pub async fn wait_for_suspense(&mut self) {
|
||||||
loop {
|
loop {
|
||||||
if !self.suspended_tasks_remaining() {
|
if !self.suspended_tasks_remaining() && !self.has_dirty_scopes() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -736,9 +736,11 @@ impl VirtualDom {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue any new events
|
// Queue any new events
|
||||||
self.queue_events();
|
self.queue_events();
|
||||||
work_done += 1;
|
work_done += 1;
|
||||||
|
|
||||||
// Once we have polled a few tasks, we manually yield to the scheduler to give it a chance to run other pending work
|
// Once we have polled a few tasks, we manually yield to the scheduler to give it a chance to run other pending work
|
||||||
if work_done > 32 {
|
if work_done > 32 {
|
||||||
yield_now().await;
|
yield_now().await;
|
||||||
|
|||||||
@@ -61,9 +61,7 @@ fn clear_error_boundary() {
|
|||||||
handle_error: |error: ErrorContext| {
|
handle_error: |error: ErrorContext| {
|
||||||
error.clear_errors();
|
error.clear_errors();
|
||||||
|
|
||||||
rsx! {
|
rsx! { "We cleared it" }
|
||||||
"We cleared it"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
ThrowsError {}
|
ThrowsError {}
|
||||||
|
|||||||
@@ -196,11 +196,13 @@ fn file_dialog_responder(
|
|||||||
|
|
||||||
let file_list = file_dialog.get_file_event();
|
let file_list = file_dialog.get_file_event();
|
||||||
|
|
||||||
|
// Get the position of the entry we're updating, so we can insert new entries in the same place
|
||||||
|
// If we can't find it, just append to the end. This is usually due to the input not being in a form element.
|
||||||
let position_of_entry = file_dialog
|
let position_of_entry = file_dialog
|
||||||
.values
|
.values
|
||||||
.iter()
|
.iter()
|
||||||
.position(|x| x.key == file_dialog.target_name)
|
.position(|x| x.key == file_dialog.target_name)
|
||||||
.unwrap_or(0);
|
.unwrap_or(file_dialog.values.len());
|
||||||
|
|
||||||
// Remove any existing entries
|
// Remove any existing entries
|
||||||
file_dialog
|
file_dialog
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ pub mod prelude {
|
|||||||
pub use dioxus_fullstack::{
|
pub use dioxus_fullstack::{
|
||||||
self as dioxus_fullstack, delete, get, patch, post, put, server, use_loader,
|
self as dioxus_fullstack, delete, get, patch, post, put, server, use_loader,
|
||||||
use_server_cached, use_server_future, HttpError, OrHttpError, ServerFnError,
|
use_server_cached, use_server_future, HttpError, OrHttpError, ServerFnError,
|
||||||
ServerFnResult,
|
ServerFnResult, StatusCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::transport::SerializeContextEntry;
|
use crate::{transport::SerializeContextEntry, Transportable};
|
||||||
use dioxus_core::use_hook;
|
use dioxus_core::use_hook;
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
|
||||||
|
|
||||||
/// This allows you to send data from the server to the client *during hydration*.
|
/// This allows you to send data from the server to the client *during hydration*.
|
||||||
/// - When compiled as server, the closure is ran and the resulting data is serialized on the server and sent to the client.
|
/// - When compiled as server, the closure is ran and the resulting data is serialized on the server and sent to the client.
|
||||||
@@ -24,26 +23,35 @@ use serde::{de::DeserializeOwned, Serialize};
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn use_server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
|
pub fn use_server_cached<O, M>(server_fn: impl Fn() -> O) -> O
|
||||||
server_fn: impl Fn() -> O,
|
where
|
||||||
) -> O {
|
O: Transportable<M> + Clone,
|
||||||
|
M: 'static,
|
||||||
|
{
|
||||||
let location = std::panic::Location::caller();
|
let location = std::panic::Location::caller();
|
||||||
use_hook(|| server_cached(server_fn, location))
|
use_hook(|| server_cached(server_fn, location))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
|
pub(crate) fn server_cached<O, M>(
|
||||||
value: impl FnOnce() -> O,
|
value: impl FnOnce() -> O,
|
||||||
#[allow(unused)] location: &'static std::panic::Location<'static>,
|
#[allow(unused)] location: &'static std::panic::Location<'static>,
|
||||||
) -> O {
|
) -> O
|
||||||
let serialize = crate::serialize_context();
|
where
|
||||||
|
O: Transportable<M> + Clone,
|
||||||
|
M: 'static,
|
||||||
|
{
|
||||||
|
let serialize = crate::transport::serialize_context();
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
let entry: SerializeContextEntry<O> = serialize.create_entry();
|
let entry: SerializeContextEntry<O> = serialize.create_entry();
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
let data = value();
|
let data = value();
|
||||||
entry.insert(&data, location);
|
entry.insert(&data, location);
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(not(feature = "server"), feature = "web"))]
|
#[cfg(all(not(feature = "server"), feature = "web"))]
|
||||||
{
|
{
|
||||||
match entry.get() {
|
match entry.get() {
|
||||||
@@ -51,51 +59,9 @@ pub(crate) fn server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
|
|||||||
Err(_) => value(),
|
Err(_) => value(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(feature = "server", feature = "web")))]
|
#[cfg(not(any(feature = "server", feature = "web")))]
|
||||||
{
|
{
|
||||||
value()
|
value()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn use_server_cached<O, M>(server_fn: impl Fn() -> O) -> O
|
|
||||||
// where
|
|
||||||
// O: Transportable<M> + Clone,
|
|
||||||
// M: 'static,
|
|
||||||
// {
|
|
||||||
// let location = std::panic::Location::caller();
|
|
||||||
// use_hook(|| server_cached(server_fn, location))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// pub(crate) fn server_cached<O, M>(
|
|
||||||
// value: impl FnOnce() -> O,
|
|
||||||
// #[allow(unused)] location: &'static std::panic::Location<'static>,
|
|
||||||
// ) -> O
|
|
||||||
// where
|
|
||||||
// O: Transportable<M> + Clone,
|
|
||||||
// M: 'static,
|
|
||||||
// {
|
|
||||||
// let serialize = crate::transport::serialize_context();
|
|
||||||
|
|
||||||
// #[allow(unused)]
|
|
||||||
// let entry: SerializeContextEntry<O> = serialize.create_entry();
|
|
||||||
|
|
||||||
// #[cfg(feature = "server")]
|
|
||||||
// {
|
|
||||||
// let data = value();
|
|
||||||
// entry.insert(&data, location);
|
|
||||||
// data
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[cfg(all(not(feature = "server"), feature = "web"))]
|
|
||||||
// {
|
|
||||||
// match entry.get() {
|
|
||||||
// Ok(value) => value,
|
|
||||||
// Err(_) => value(),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[cfg(not(any(feature = "server", feature = "web")))]
|
|
||||||
// {
|
|
||||||
// value()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
|
use crate::Transportable;
|
||||||
use dioxus_core::{suspend, use_hook, RenderError};
|
use dioxus_core::{suspend, use_hook, RenderError};
|
||||||
use dioxus_hooks::*;
|
use dioxus_hooks::*;
|
||||||
use dioxus_signals::ReadableExt;
|
use dioxus_signals::ReadableExt;
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
/// Runs a future and returns a result with a resource if the future is finished or a suspended error if it is still running. The future may run on the server *during hydration*.
|
/// Runs a future with a manual list of dependencies and returns a resource with the result if the future is finished or a suspended error if it is still running.
|
||||||
/// - When compiled as server, the closure's future is ran to completion and the resulting data is serialized on the server and sent to the client.
|
|
||||||
/// - When compiled as web client, the data is deserialized from the server if already available, otherwise runs on the client. Data is usually only available if this hook exists in a component during hydration.
|
|
||||||
/// - When otherwise compiled, the closure is run directly with no serialization.
|
|
||||||
///
|
///
|
||||||
/// On the server, this will wait until the future is resolved before continuing to render. Thus, this blocks other subsequent server hooks. The result is cached.
|
///
|
||||||
|
/// On the server, this will wait until the future is resolved before continuing to render. When the future is resolved, the result will be serialized into the page and hydrated on the client without rerunning the future.
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// <div class="warning">
|
/// <div class="warning">
|
||||||
@@ -59,19 +57,21 @@ use std::future::Future;
|
|||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn use_server_future<T, F>(
|
pub fn use_server_future<T, F, M>(
|
||||||
mut future: impl FnMut() -> F + 'static,
|
mut future: impl FnMut() -> F + 'static,
|
||||||
) -> Result<Resource<T>, RenderError>
|
) -> Result<Resource<T>, RenderError>
|
||||||
where
|
where
|
||||||
T: Serialize + DeserializeOwned + 'static,
|
|
||||||
F: Future<Output = T> + 'static,
|
F: Future<Output = T> + 'static,
|
||||||
|
T: Transportable<M>,
|
||||||
|
M: 'static,
|
||||||
{
|
{
|
||||||
let serialize_context = use_hook(crate::serialize_context);
|
let serialize_context = use_hook(crate::transport::serialize_context);
|
||||||
|
|
||||||
// We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
// We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
let storage_entry: crate::SerializeContextEntry<T> =
|
let storage_entry: crate::transport::SerializeContextEntry<T> =
|
||||||
use_hook(|| serialize_context.create_entry());
|
use_hook(|| serialize_context.create_entry());
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
@@ -100,7 +100,9 @@ where
|
|||||||
Some(Ok(o)) => return o,
|
Some(Ok(o)) => return o,
|
||||||
|
|
||||||
// The data is still pending from the server. Don't try to resolve it on the client
|
// The data is still pending from the server. Don't try to resolve it on the client
|
||||||
Some(Err(crate::TakeDataError::DataPending)) => std::future::pending::<()>().await,
|
Some(Err(crate::transport::TakeDataError::DataPending)) => {
|
||||||
|
std::future::pending::<()>().await
|
||||||
|
}
|
||||||
|
|
||||||
// The data was not available on the server, rerun the future
|
// The data was not available on the server, rerun the future
|
||||||
Some(Err(_)) => {}
|
Some(Err(_)) => {}
|
||||||
@@ -135,296 +137,3 @@ where
|
|||||||
|
|
||||||
Ok(resource)
|
Ok(resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
// use dioxus_core::{suspend, use_hook, RenderError};
|
|
||||||
// use dioxus_hooks::*;
|
|
||||||
// use dioxus_signals::ReadableExt;
|
|
||||||
// use serde::{de::DeserializeOwned, Serialize};
|
|
||||||
// use std::future::Future;
|
|
||||||
|
|
||||||
// /// Runs a future with a manual list of dependencies and returns a resource with the result if the future is finished or a suspended error if it is still running.
|
|
||||||
// ///
|
|
||||||
// ///
|
|
||||||
// /// On the server, this will wait until the future is resolved before continuing to render. When the future is resolved, the result will be serialized into the page and hydrated on the client without rerunning the future.
|
|
||||||
// ///
|
|
||||||
// ///
|
|
||||||
// /// <div class="warning">
|
|
||||||
// ///
|
|
||||||
// /// Unlike [`use_resource`] dependencies are only tracked inside the function that spawns the async block, not the async block itself.
|
|
||||||
// ///
|
|
||||||
// /// ```rust, no_run
|
|
||||||
// /// # use dioxus::prelude::*;
|
|
||||||
// /// // ❌ The future inside of use_server_future is not reactive
|
|
||||||
// /// let id = use_signal(|| 0);
|
|
||||||
// /// use_server_future(move || {
|
|
||||||
// /// async move {
|
|
||||||
// /// // But the future is not reactive which means that the future will not subscribe to any reads here
|
|
||||||
// /// println!("{id}");
|
|
||||||
// /// }
|
|
||||||
// /// });
|
|
||||||
// /// // ✅ The closure that creates the future for use_server_future is reactive
|
|
||||||
// /// let id = use_signal(|| 0);
|
|
||||||
// /// use_server_future(move || {
|
|
||||||
// /// // The closure itself is reactive which means the future will subscribe to any signals you read here
|
|
||||||
// /// let cloned_id = id();
|
|
||||||
// /// async move {
|
|
||||||
// /// // But the future is not reactive which means that the future will not subscribe to any reads here
|
|
||||||
// /// println!("{cloned_id}");
|
|
||||||
// /// }
|
|
||||||
// /// });
|
|
||||||
// /// ```
|
|
||||||
// ///
|
|
||||||
// /// </div>
|
|
||||||
// ///
|
|
||||||
// /// # Example
|
|
||||||
// ///
|
|
||||||
// /// ```rust, no_run
|
|
||||||
// /// # use dioxus::prelude::*;
|
|
||||||
// /// # async fn fetch_article(id: u32) -> String { unimplemented!() }
|
|
||||||
// /// use dioxus::prelude::*;
|
|
||||||
// ///
|
|
||||||
// /// fn App() -> Element {
|
|
||||||
// /// let mut article_id = use_signal(|| 0);
|
|
||||||
// /// // `use_server_future` will spawn a task that runs on the server and serializes the result to send to the client.
|
|
||||||
// /// // The future will rerun any time the
|
|
||||||
// /// // Since we bubble up the suspense with `?`, the server will wait for the future to resolve before rendering
|
|
||||||
// /// let article = use_server_future(move || fetch_article(article_id()))?;
|
|
||||||
// ///
|
|
||||||
// /// rsx! {
|
|
||||||
// /// "{article().unwrap()}"
|
|
||||||
// /// }
|
|
||||||
// /// }
|
|
||||||
// /// ```
|
|
||||||
// #[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
|
|
||||||
// #[track_caller]
|
|
||||||
// pub fn use_server_future<T, F>(
|
|
||||||
// mut future: impl FnMut() -> F + 'static,
|
|
||||||
// ) -> Result<Resource<T>, RenderError>
|
|
||||||
// where
|
|
||||||
// T: Serialize + DeserializeOwned + 'static,
|
|
||||||
// F: Future<Output = T> + 'static,
|
|
||||||
// {
|
|
||||||
// let serialize_context = use_hook(crate::serialize_context);
|
|
||||||
|
|
||||||
// // We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
|
||||||
// #[allow(unused)]
|
|
||||||
// let storage_entry: crate::SerializeContextEntry<T> =
|
|
||||||
// use_hook(|| serialize_context.create_entry());
|
|
||||||
|
|
||||||
// #[cfg(feature = "server")]
|
|
||||||
// let caller = std::panic::Location::caller();
|
|
||||||
|
|
||||||
// // If this is the first run and we are on the web client, the data might be cached
|
|
||||||
// #[cfg(feature = "web")]
|
|
||||||
// let initial_web_result =
|
|
||||||
// use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(Some(storage_entry.get()))));
|
|
||||||
|
|
||||||
// let resource = use_resource(move || {
|
|
||||||
// #[cfg(feature = "server")]
|
|
||||||
// let storage_entry = storage_entry.clone();
|
|
||||||
|
|
||||||
// let user_fut = future();
|
|
||||||
|
|
||||||
// #[cfg(feature = "web")]
|
|
||||||
// let initial_web_result = initial_web_result.clone();
|
|
||||||
|
|
||||||
// #[allow(clippy::let_and_return)]
|
|
||||||
// async move {
|
|
||||||
// // If this is the first run and we are on the web client, the data might be cached
|
|
||||||
// #[cfg(feature = "web")]
|
|
||||||
// match initial_web_result.take() {
|
|
||||||
// // The data was deserialized successfully from the server
|
|
||||||
// Some(Ok(o)) => return o,
|
|
||||||
|
|
||||||
// // The data is still pending from the server. Don't try to resolve it on the client
|
|
||||||
// Some(Err(crate::TakeDataError::DataPending)) => std::future::pending::<()>().await,
|
|
||||||
|
|
||||||
// // The data was not available on the server, rerun the future
|
|
||||||
// Some(Err(_)) => {}
|
|
||||||
|
|
||||||
// // This isn't the first run, so we don't need do anything
|
|
||||||
// None => {}
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Otherwise just run the future itself
|
|
||||||
// let out = user_fut.await;
|
|
||||||
|
|
||||||
// // If this is the first run and we are on the server, cache the data in the slot we reserved for it
|
|
||||||
// #[cfg(feature = "server")]
|
|
||||||
// storage_entry.insert(&out, caller);
|
|
||||||
|
|
||||||
// out
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // On the first run, force this task to be polled right away in case its value is ready
|
|
||||||
// use_hook(|| {
|
|
||||||
// let _ = resource.task().poll_now();
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Suspend if the value isn't ready
|
|
||||||
// if resource.state().cloned() == UseResourceState::Pending {
|
|
||||||
// let task = resource.task();
|
|
||||||
// if !task.paused() {
|
|
||||||
// return Err(suspend(task).unwrap_err());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Ok(resource)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // use dioxus_core::{suspend, use_hook, RenderError};
|
|
||||||
// // use dioxus_hooks::*;
|
|
||||||
// // use dioxus_signals::ReadableExt;
|
|
||||||
// // use std::future::Future;
|
|
||||||
// // // use crate::Transportable;
|
|
||||||
|
|
||||||
// // /// Runs a future with a manual list of dependencies and returns a resource with the result if the future is finished or a suspended error if it is still running.
|
|
||||||
// // ///
|
|
||||||
// // ///
|
|
||||||
// // /// On the server, this will wait until the future is resolved before continuing to render. When the future is resolved, the result will be serialized into the page and hydrated on the client without rerunning the future.
|
|
||||||
// // ///
|
|
||||||
// // ///
|
|
||||||
// // /// <div class="warning">
|
|
||||||
// // ///
|
|
||||||
// // /// Unlike [`use_resource`] dependencies are only tracked inside the function that spawns the async block, not the async block itself.
|
|
||||||
// // ///
|
|
||||||
// // /// ```rust, no_run
|
|
||||||
// // /// # use dioxus::prelude::*;
|
|
||||||
// // /// // ❌ The future inside of use_server_future is not reactive
|
|
||||||
// // /// let id = use_signal(|| 0);
|
|
||||||
// // /// use_server_future(move || {
|
|
||||||
// // /// async move {
|
|
||||||
// // /// // But the future is not reactive which means that the future will not subscribe to any reads here
|
|
||||||
// // /// println!("{id}");
|
|
||||||
// // /// }
|
|
||||||
// // /// });
|
|
||||||
// // /// // ✅ The closure that creates the future for use_server_future is reactive
|
|
||||||
// // /// let id = use_signal(|| 0);
|
|
||||||
// // /// use_server_future(move || {
|
|
||||||
// // /// // The closure itself is reactive which means the future will subscribe to any signals you read here
|
|
||||||
// // /// let cloned_id = id();
|
|
||||||
// // /// async move {
|
|
||||||
// // /// // But the future is not reactive which means that the future will not subscribe to any reads here
|
|
||||||
// // /// println!("{cloned_id}");
|
|
||||||
// // /// }
|
|
||||||
// // /// });
|
|
||||||
// // /// ```
|
|
||||||
// // ///
|
|
||||||
// // /// </div>
|
|
||||||
// // ///
|
|
||||||
// // /// # Example
|
|
||||||
// // ///
|
|
||||||
// // /// ```rust, no_run
|
|
||||||
// // /// # use dioxus::prelude::*;
|
|
||||||
// // /// # async fn fetch_article(id: u32) -> String { unimplemented!() }
|
|
||||||
// // /// use dioxus::prelude::*;
|
|
||||||
// // ///
|
|
||||||
// // /// fn App() -> Element {
|
|
||||||
// // /// let mut article_id = use_signal(|| 0);
|
|
||||||
// // /// // `use_server_future` will spawn a task that runs on the server and serializes the result to send to the client.
|
|
||||||
// // /// // The future will rerun any time the
|
|
||||||
// // /// // Since we bubble up the suspense with `?`, the server will wait for the future to resolve before rendering
|
|
||||||
// // /// let article = use_server_future(move || fetch_article(article_id()))?;
|
|
||||||
// // ///
|
|
||||||
// // /// rsx! {
|
|
||||||
// // /// "{article().unwrap()}"
|
|
||||||
// // /// }
|
|
||||||
// // /// }
|
|
||||||
// // /// ```
|
|
||||||
// // #[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
|
|
||||||
// // #[track_caller]
|
|
||||||
// // pub fn use_server_future<T, F, M>(
|
|
||||||
// // future: impl FnMut() -> F + 'static,
|
|
||||||
// // ) -> Result<Resource<T>, RenderError>
|
|
||||||
// // where
|
|
||||||
// // F: Future<Output = T> + 'static,
|
|
||||||
// // T: Transportable<M>,
|
|
||||||
// // M: 'static,
|
|
||||||
// // {
|
|
||||||
// // let resource = use_server_future_unsuspended(future);
|
|
||||||
|
|
||||||
// // // Suspend if the value isn't ready
|
|
||||||
// // if resource.state().cloned() == UseResourceState::Pending {
|
|
||||||
// // let task = resource.task();
|
|
||||||
// // if !task.paused() {
|
|
||||||
// // return Err(suspend(task).unwrap_err());
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // Ok(resource)
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // /// Like [`use_server_future`] but does not suspend if the future is still running.
|
|
||||||
// // ///
|
|
||||||
// // /// You need to manage suspending yourself by checking the resource state.
|
|
||||||
// // pub fn use_server_future_unsuspended<T, F, M>(
|
|
||||||
// // mut future: impl FnMut() -> F + 'static,
|
|
||||||
// // ) -> Resource<T>
|
|
||||||
// // where
|
|
||||||
// // F: Future<Output = T> + 'static,
|
|
||||||
// // T: Transportable<M>,
|
|
||||||
// // M: 'static,
|
|
||||||
// // {
|
|
||||||
// // let serialize_context = use_hook(crate::transport::serialize_context);
|
|
||||||
|
|
||||||
// // // We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
|
||||||
// // #[allow(unused)]
|
|
||||||
// // let storage_entry: crate::transport::SerializeContextEntry<T> =
|
|
||||||
// // use_hook(|| serialize_context.create_entry());
|
|
||||||
|
|
||||||
// // #[cfg(feature = "server")]
|
|
||||||
// // let caller = std::panic::Location::caller();
|
|
||||||
|
|
||||||
// // // If this is the first run and we are on the web client, the data might be cached
|
|
||||||
// // #[cfg(feature = "web")]
|
|
||||||
// // let initial_web_result =
|
|
||||||
// // use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(Some(storage_entry.get()))));
|
|
||||||
|
|
||||||
// // let resource = use_resource(move || {
|
|
||||||
// // #[cfg(feature = "server")]
|
|
||||||
// // let storage_entry = storage_entry.clone();
|
|
||||||
|
|
||||||
// // let user_fut = future();
|
|
||||||
|
|
||||||
// // #[cfg(feature = "web")]
|
|
||||||
// // let initial_web_result = initial_web_result.clone();
|
|
||||||
|
|
||||||
// // #[allow(clippy::let_and_return)]
|
|
||||||
// // async move {
|
|
||||||
// // // If this is the first run and we are on the web client, the data might be cached
|
|
||||||
// // #[cfg(feature = "web")]
|
|
||||||
// // match initial_web_result.take() {
|
|
||||||
// // // The data was deserialized successfully from the server
|
|
||||||
// // Some(Ok(o)) => return o,
|
|
||||||
|
|
||||||
// // // The data is still pending from the server. Don't try to resolve it on the client
|
|
||||||
// // Some(Err(crate::transport::TakeDataError::DataPending)) => {
|
|
||||||
// // std::future::pending::<()>().await
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // // The data was not available on the server, rerun the future
|
|
||||||
// // Some(Err(_)) => {}
|
|
||||||
|
|
||||||
// // // This isn't the first run, so we don't need do anything
|
|
||||||
// // None => {}
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // // Otherwise just run the future itself
|
|
||||||
// // let out = user_fut.await;
|
|
||||||
|
|
||||||
// // // If this is the first run and we are on the server, cache the data in the slot we reserved for it
|
|
||||||
// // #[cfg(feature = "server")]
|
|
||||||
// // storage_entry.insert(&out, caller);
|
|
||||||
|
|
||||||
// // out
|
|
||||||
// // }
|
|
||||||
// // });
|
|
||||||
|
|
||||||
// // // On the first run, force this task to be polled right away in case its value is ready
|
|
||||||
// // use_hook(|| {
|
|
||||||
// // let _ = resource.task().poll_now();
|
|
||||||
// // });
|
|
||||||
|
|
||||||
// // resource
|
|
||||||
// // }
|
|
||||||
@@ -1,42 +1,39 @@
|
|||||||
use crate::ServerFnError;
|
use crate::{HttpError, ServerFnError};
|
||||||
use axum_core::{extract::FromRequest, response::IntoResponse};
|
use axum_core::{extract::FromRequest, response::IntoResponse};
|
||||||
use dioxus_core::try_consume_context;
|
use dioxus_core::{try_consume_context, CapturedError};
|
||||||
use dioxus_signals::{ReadableExt, Signal, WritableExt};
|
use dioxus_signals::{ReadableExt, Signal, WritableExt};
|
||||||
use http::request::Parts;
|
use http::request::Parts;
|
||||||
|
use http::StatusCode;
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
/// The status of the streaming response
|
/// The context provided by dioxus fullstack for server-side rendering.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
///
|
||||||
pub enum StreamingStatus {
|
/// This context will only be set on the server during a streaming response.
|
||||||
/// The initial chunk is still being rendered. The http response parts can still be modified at this point.
|
|
||||||
RenderingInitialChunk,
|
|
||||||
|
|
||||||
/// The initial chunk has been committed and the response is now streaming. The http response parts
|
|
||||||
/// have already been sent to the client and can no longer be modified.
|
|
||||||
InitialChunkCommitted,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The context dioxus fullstack provides for the status of streaming responses on the server
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct StreamingContext {
|
pub struct FullstackContext {
|
||||||
current_status: Signal<StreamingStatus>,
|
current_status: Signal<StreamingStatus>,
|
||||||
request_headers: Rc<RefCell<http::request::Parts>>,
|
request_headers: Rc<RefCell<http::request::Parts>>,
|
||||||
|
route_http_status: Signal<HttpError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for StreamingContext {
|
impl PartialEq for FullstackContext {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.current_status == other.current_status
|
self.current_status == other.current_status
|
||||||
&& Rc::ptr_eq(&self.request_headers, &other.request_headers)
|
&& Rc::ptr_eq(&self.request_headers, &other.request_headers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StreamingContext {
|
impl FullstackContext {
|
||||||
/// Create a new streaming context. You should not need to call this directly. Dioxus fullstack will
|
/// Create a new streaming context. You should not need to call this directly. Dioxus fullstack will
|
||||||
/// provide this context for you.
|
/// provide this context for you.
|
||||||
pub fn new(parts: Parts) -> Self {
|
pub fn new(parts: Parts) -> Self {
|
||||||
Self {
|
Self {
|
||||||
current_status: Signal::new(StreamingStatus::RenderingInitialChunk),
|
current_status: Signal::new(StreamingStatus::RenderingInitialChunk),
|
||||||
request_headers: Rc::new(RefCell::new(parts)),
|
request_headers: Rc::new(RefCell::new(parts)),
|
||||||
|
route_http_status: Signal::new(HttpError {
|
||||||
|
status: http::StatusCode::OK,
|
||||||
|
message: None,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +49,7 @@ impl StreamingContext {
|
|||||||
|
|
||||||
/// Get the current status of the streaming response. This method is reactive and will cause
|
/// Get the current status of the streaming response. This method is reactive and will cause
|
||||||
/// the current reactive context to rerun when the status changes.
|
/// the current reactive context to rerun when the status changes.
|
||||||
pub fn current_status(&self) -> StreamingStatus {
|
pub fn streaming_state(&self) -> StreamingStatus {
|
||||||
*self.current_status.read()
|
*self.current_status.read()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +62,7 @@ impl StreamingContext {
|
|||||||
/// since it's assumed that rendering the app is done under a `GET` request.
|
/// since it's assumed that rendering the app is done under a `GET` request.
|
||||||
pub async fn extract<T: FromRequest<(), M>, M>() -> Result<T, ServerFnError> {
|
pub async fn extract<T: FromRequest<(), M>, M>() -> Result<T, ServerFnError> {
|
||||||
let this = Self::current()
|
let this = Self::current()
|
||||||
.ok_or_else(|| ServerFnError::new("No StreamingContext found".to_string()))?;
|
.ok_or_else(|| ServerFnError::new("No FullstackContext found".to_string()))?;
|
||||||
|
|
||||||
let parts = this.request_headers.borrow_mut().clone();
|
let parts = this.request_headers.borrow_mut().clone();
|
||||||
let request =
|
let request =
|
||||||
@@ -79,18 +76,66 @@ impl StreamingContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current `StreamingContext` if it exists. This will return `None` if called on the client
|
/// Get the current `FullstackContext` if it exists. This will return `None` if called on the client
|
||||||
/// or outside of a streaming response on the server.
|
/// or outside of a streaming response on the server.
|
||||||
pub fn current() -> Option<Self> {
|
pub fn current() -> Option<Self> {
|
||||||
if let Some(rt) = dioxus_core::Runtime::try_current() {
|
if let Some(rt) = dioxus_core::Runtime::try_current() {
|
||||||
let id = rt.try_current_scope_id()?;
|
let id = rt.try_current_scope_id()?;
|
||||||
if let Some(ctx) = rt.consume_context::<StreamingContext>(id) {
|
if let Some(ctx) = rt.consume_context::<FullstackContext>(id) {
|
||||||
return Some(ctx);
|
return Some(ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the current HTTP status for the route. This will default to 200 OK, but can be modified
|
||||||
|
/// by calling `FullstackContext::commit_error_status` with an error.
|
||||||
|
pub fn current_http_status(&self) -> HttpError {
|
||||||
|
self.route_http_status.read().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_http_status(&mut self, status: HttpError) {
|
||||||
|
self.route_http_status.set(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the current HTTP status for the route. This will be used when committing the response
|
||||||
|
/// to the client.
|
||||||
|
pub fn commit_http_status(status: StatusCode, message: Option<String>) {
|
||||||
|
if let Some(mut ctx) = Self::current() {
|
||||||
|
ctx.set_current_http_status(HttpError { status, message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commit the CapturedError as the current HTTP status for the route.
|
||||||
|
/// This will attempt to downcast the error to known types and set the appropriate
|
||||||
|
/// status code. If the error type is unknown, it will default to
|
||||||
|
/// `StatusCode::INTERNAL_SERVER_ERROR`.
|
||||||
|
pub fn commit_error_status(error: impl Into<CapturedError>) -> HttpError {
|
||||||
|
let error = error.into();
|
||||||
|
let status = status_code_from_error(&error);
|
||||||
|
let http_error = HttpError {
|
||||||
|
status,
|
||||||
|
message: Some(error.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(mut ctx) = Self::current() {
|
||||||
|
ctx.set_current_http_status(http_error.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
http_error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The status of the streaming response
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum StreamingStatus {
|
||||||
|
/// The initial chunk is still being rendered. The http response parts can still be modified at this point.
|
||||||
|
RenderingInitialChunk,
|
||||||
|
|
||||||
|
/// The initial chunk has been committed and the response is now streaming. The http response parts
|
||||||
|
/// have already been sent to the client and can no longer be modified.
|
||||||
|
InitialChunkCommitted,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Commit the initial chunk of the response. This will be called automatically if you are using the
|
/// Commit the initial chunk of the response. This will be called automatically if you are using the
|
||||||
@@ -113,7 +158,7 @@ impl StreamingContext {
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn commit_initial_chunk() {
|
pub fn commit_initial_chunk() {
|
||||||
crate::history::finalize_route();
|
crate::history::finalize_route();
|
||||||
if let Some(mut streaming) = try_consume_context::<StreamingContext>() {
|
if let Some(mut streaming) = try_consume_context::<FullstackContext>() {
|
||||||
streaming.commit_initial_chunk();
|
streaming.commit_initial_chunk();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,9 +186,34 @@ pub fn commit_initial_chunk() {
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn current_status() -> StreamingStatus {
|
pub fn current_status() -> StreamingStatus {
|
||||||
if let Some(streaming) = try_consume_context::<StreamingContext>() {
|
if let Some(streaming) = try_consume_context::<FullstackContext>() {
|
||||||
streaming.current_status()
|
streaming.streaming_state()
|
||||||
} else {
|
} else {
|
||||||
StreamingStatus::InitialChunkCommitted
|
StreamingStatus::InitialChunkCommitted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a `CapturedError` into an appropriate HTTP status code.
|
||||||
|
///
|
||||||
|
/// This will attempt to downcast the error to known types and return a corresponding status code.
|
||||||
|
/// If the error type is unknown, it will default to `StatusCode::INTERNAL_SERVER_ERROR`.
|
||||||
|
pub fn status_code_from_error(error: &CapturedError) -> StatusCode {
|
||||||
|
if let Some(err) = error.downcast_ref::<ServerFnError>() {
|
||||||
|
match err {
|
||||||
|
ServerFnError::ServerError {
|
||||||
|
code: Some(code), ..
|
||||||
|
} => return StatusCode::from_u16(*code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
|
_ => return StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(err) = error.downcast_ref::<StatusCode>() {
|
||||||
|
return *err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(err) = error.downcast_ref::<HttpError>() {
|
||||||
|
return err.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
}
|
||||||
@@ -3,12 +3,8 @@
|
|||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use dioxus_core::CapturedError;
|
use dioxus_core::CapturedError;
|
||||||
use serde::Serialize;
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use std::{cell::RefCell, io::Cursor, rc::Rc};
|
use std::{cell::RefCell, io::Cursor, rc::Rc, sync::Arc};
|
||||||
// use base64::Engine;
|
|
||||||
// use dioxus_core::{CapturedError, Error};
|
|
||||||
// use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
|
||||||
// use std::{cell::RefCell, io::Cursor, rc::Rc, sync::Arc};
|
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
#[cfg(feature = "web")]
|
||||||
thread_local! {
|
thread_local! {
|
||||||
@@ -88,8 +84,7 @@ impl HydrationContext {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub(crate) fn insert<T: Transportable<M>, M: 'static>(
|
pub(crate) fn insert<T: Transportable<M>, M: 'static>(
|
||||||
pub(crate) fn insert<T: Serialize>(
|
|
||||||
&self,
|
&self,
|
||||||
id: usize,
|
id: usize,
|
||||||
value: &T,
|
value: &T,
|
||||||
@@ -98,8 +93,7 @@ impl HydrationContext {
|
|||||||
self.data.borrow_mut().insert(id, value, location);
|
self.data.borrow_mut().insert(id, value, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub(crate) fn get<T: Transportable<M>, M: 'static>(
|
pub(crate) fn get<T: Transportable<M>, M: 'static>(
|
||||||
pub(crate) fn get<T: serde::de::DeserializeOwned>(
|
|
||||||
&self,
|
&self,
|
||||||
id: usize,
|
id: usize,
|
||||||
) -> Result<T, TakeDataError> {
|
) -> Result<T, TakeDataError> {
|
||||||
@@ -134,35 +128,20 @@ impl<T> Clone for SerializeContextEntry<T> {
|
|||||||
|
|
||||||
impl<T> SerializeContextEntry<T> {
|
impl<T> SerializeContextEntry<T> {
|
||||||
/// Insert data into an entry that was created with [`HydrationContext::create_entry`]
|
/// Insert data into an entry that was created with [`HydrationContext::create_entry`]
|
||||||
pub fn insert(&self, value: &T, location: &'static std::panic::Location<'static>)
|
pub fn insert<M: 'static>(self, value: &T, location: &'static std::panic::Location<'static>)
|
||||||
where
|
where
|
||||||
T: Serialize,
|
T: Transportable<M>,
|
||||||
{
|
{
|
||||||
self.context.insert(self.index, value, location);
|
self.context.insert(self.index, value, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Grab the data from the serialize context
|
/// Grab the data from the serialize context
|
||||||
pub fn get(&self) -> Result<T, TakeDataError>
|
pub fn get<M: 'static>(&self) -> Result<T, TakeDataError>
|
||||||
where
|
where
|
||||||
T: serde::de::DeserializeOwned,
|
T: Transportable<M>,
|
||||||
{
|
{
|
||||||
self.context.get(self.index)
|
self.context.get(self.index)
|
||||||
}
|
}
|
||||||
// /// Insert data into an entry that was created with [`HydrationContext::create_entry`]
|
|
||||||
// pub fn insert<M: 'static>(self, value: &T, location: &'static std::panic::Location<'static>)
|
|
||||||
// where
|
|
||||||
// T: Transportable<M>,
|
|
||||||
// {
|
|
||||||
// self.context.insert(self.index, value, location);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// Grab the data from the serialize context
|
|
||||||
// pub fn get<M: 'static>(&self) -> Result<T, TakeDataError>
|
|
||||||
// where
|
|
||||||
// T: Transportable<M>,
|
|
||||||
// {
|
|
||||||
// self.context.get(self.index)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the client is currently rendering a component for hydration. Always returns true on the server.
|
/// Check if the client is currently rendering a component for hydration. Always returns true on the server.
|
||||||
@@ -276,16 +255,13 @@ impl HTMLData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Insert data into an entry that was created with [`Self::create_entry`]
|
/// Insert data into an entry that was created with [`Self::create_entry`]
|
||||||
// fn insert<T: Transportable<M>, M: 'static>(
|
fn insert<T: Transportable<M>, M: 'static>(
|
||||||
fn insert<T: Serialize>(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
id: usize,
|
id: usize,
|
||||||
value: &T,
|
value: &T,
|
||||||
#[allow(unused)] location: &'static std::panic::Location<'static>,
|
#[allow(unused)] location: &'static std::panic::Location<'static>,
|
||||||
) {
|
) {
|
||||||
// let serialized = value.transport_to_bytes();
|
let serialized = value.transport_to_bytes();
|
||||||
let mut serialized = Vec::new();
|
|
||||||
ciborium::into_writer(value, &mut serialized).unwrap();
|
|
||||||
self.data[id] = Some(serialized);
|
self.data[id] = Some(serialized);
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
{
|
{
|
||||||
@@ -295,8 +271,7 @@ impl HTMLData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the data from the serialize context
|
/// Get the data from the serialize context
|
||||||
// fn get<T: Transportable<M>, M: 'static>(&self, index: usize) -> Result<T, TakeDataError> {
|
fn get<T: Transportable<M>, M: 'static>(&self, index: usize) -> Result<T, TakeDataError> {
|
||||||
fn get<T: serde::de::DeserializeOwned>(&self, index: usize) -> Result<T, TakeDataError> {
|
|
||||||
if index >= self.data.len() {
|
if index >= self.data.len() {
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
"Tried to take more data than was available, len: {}, index: {}; This is normal if the server function was started on the client, but may indicate a bug if the server function result should be deserialized from the server",
|
"Tried to take more data than was available, len: {}, index: {}; This is normal if the server function was started on the client, but may indicate a bug if the server function result should be deserialized from the server",
|
||||||
@@ -307,8 +282,7 @@ impl HTMLData {
|
|||||||
}
|
}
|
||||||
let bytes = self.data[index].as_ref();
|
let bytes = self.data[index].as_ref();
|
||||||
match bytes {
|
match bytes {
|
||||||
Some(bytes) => match ciborium::from_reader(Cursor::new(bytes)) {
|
Some(bytes) => match T::transport_from_bytes(bytes) {
|
||||||
// Some(bytes) => match T::transport_from_bytes(bytes) {
|
|
||||||
Ok(x) => Ok(x),
|
Ok(x) => Ok(x),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@@ -453,119 +427,106 @@ pub fn head_element_hydration_entry() -> SerializeContextEntry<bool> {
|
|||||||
serialize_context().create_entry()
|
serialize_context().create_entry()
|
||||||
}
|
}
|
||||||
|
|
||||||
// /// An error that can occur when trying to take data from the server
|
/// A `Transportable` type can be safely transported from the server to the client, and be used for
|
||||||
// #[derive(thiserror::Error, Debug)]
|
/// hydration. Not all types can sensibly be transported, but many can. This trait makes it possible
|
||||||
// pub enum TakeDataError {
|
/// to customize how types are transported which helps for non-serializable types like `dioxus_core::Error`.
|
||||||
// /// Deserializing the data failed
|
///
|
||||||
// #[error("DeserializationError: {0}")]
|
/// By default, all types that implement `Serialize` and `DeserializeOwned` are transportable.
|
||||||
// DeserializationError(ciborium::de::Error<std::io::Error>),
|
///
|
||||||
|
/// You can also implement `Transportable` for `Result<T, dioxus_core::Error>` where `T` is
|
||||||
|
/// `Serialize` and `DeserializeOwned` to allow transporting results that may contain errors.
|
||||||
|
///
|
||||||
|
/// Note that transporting a `Result<T, dioxus_core::Error>` will lose various aspects of the original
|
||||||
|
/// `dioxus_core::Error` such as backtraces and source errors, but will preserve the error message.
|
||||||
|
pub trait Transportable<M = ()>: 'static {
|
||||||
|
/// Serialize the type to a byte vector for transport
|
||||||
|
fn transport_to_bytes(&self) -> Vec<u8>;
|
||||||
|
|
||||||
// /// No data was available
|
/// Deserialize the type from a byte slice
|
||||||
// #[error("DataNotAvailable")]
|
fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
|
||||||
// DataNotAvailable,
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
// /// The server serialized a placeholder for the data, but it isn't available yet
|
impl<T> Transportable<()> for T
|
||||||
// #[error("DataPending")]
|
where
|
||||||
// DataPending,
|
T: Serialize + DeserializeOwned + 'static,
|
||||||
// }
|
{
|
||||||
|
fn transport_to_bytes(&self) -> Vec<u8> {
|
||||||
|
let mut serialized = Vec::new();
|
||||||
|
ciborium::into_writer(self, &mut serialized).unwrap();
|
||||||
|
serialized
|
||||||
|
}
|
||||||
|
|
||||||
// /// A `Transportable` type can be safely transported from the server to the client, and be used for
|
fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
|
||||||
// /// hydration. Not all types can sensibly be transported, but many can. This trait makes it possible
|
where
|
||||||
// /// to customize how types are transported which helps for non-serializable types like `dioxus_core::Error`.
|
Self: Sized,
|
||||||
// ///
|
{
|
||||||
// /// By default, all types that implement `Serialize` and `DeserializeOwned` are transportable.
|
ciborium::from_reader(Cursor::new(bytes))
|
||||||
// ///
|
}
|
||||||
// /// You can also implement `Transportable` for `Result<T, dioxus_core::Error>` where `T` is
|
}
|
||||||
// /// `Serialize` and `DeserializeOwned` to allow transporting results that may contain errors.
|
|
||||||
// ///
|
|
||||||
// /// Note that transporting a `Result<T, dioxus_core::Error>` will lose various aspects of the original
|
|
||||||
// /// `dioxus_core::Error` such as backtraces and source errors, but will preserve the error message.
|
|
||||||
// pub trait Transportable<M = ()>: 'static {
|
|
||||||
// /// Serialize the type to a byte vector for transport
|
|
||||||
// fn transport_to_bytes(&self) -> Vec<u8>;
|
|
||||||
|
|
||||||
// /// Deserialize the type from a byte slice
|
#[derive(Serialize, Deserialize)]
|
||||||
// fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
|
struct TransportResultErr<T> {
|
||||||
// where
|
error: Result<T, CapturedError>,
|
||||||
// Self: Sized;
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// impl<T> Transportable<()> for T
|
#[doc(hidden)]
|
||||||
// where
|
pub struct TransportViaErrMarker;
|
||||||
// T: Serialize + DeserializeOwned + 'static,
|
|
||||||
// {
|
|
||||||
// fn transport_to_bytes(&self) -> Vec<u8> {
|
|
||||||
// let mut serialized = Vec::new();
|
|
||||||
// ciborium::into_writer(self, &mut serialized).unwrap();
|
|
||||||
// serialized
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
|
impl<T> Transportable<TransportViaErrMarker> for Result<T, dioxus_core::Error>
|
||||||
// where
|
where
|
||||||
// Self: Sized,
|
T: Serialize + DeserializeOwned + 'static,
|
||||||
// {
|
{
|
||||||
// ciborium::from_reader(Cursor::new(bytes))
|
fn transport_to_bytes(&self) -> Vec<u8> {
|
||||||
// }
|
let err = TransportResultErr {
|
||||||
// }
|
error: self
|
||||||
|
.as_ref()
|
||||||
|
.map_err(|e| CapturedError::from_display(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
// #[derive(Serialize, Deserialize)]
|
let mut serialized = Vec::new();
|
||||||
// struct TransportResultErr<T> {
|
ciborium::into_writer(&err, &mut serialized).unwrap();
|
||||||
// error: Result<T, CapturedError>,
|
serialized
|
||||||
// }
|
}
|
||||||
|
|
||||||
// #[doc(hidden)]
|
fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
|
||||||
// pub struct TransportViaErrMarker;
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let err: TransportResultErr<T> = ciborium::from_reader(Cursor::new(bytes))?;
|
||||||
|
match err.error {
|
||||||
|
Ok(value) => Ok(Ok(value)),
|
||||||
|
Err(captured) => Ok(Err(dioxus_core::Error::msg(captured.to_string()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// impl<T> Transportable<TransportViaErrMarker> for Result<T, dioxus_core::Error>
|
#[doc(hidden)]
|
||||||
// where
|
pub struct TransportCapturedError;
|
||||||
// T: Serialize + DeserializeOwned + 'static,
|
#[derive(Serialize, Deserialize)]
|
||||||
// {
|
struct TransportError {
|
||||||
// fn transport_to_bytes(&self) -> Vec<u8> {
|
error: String,
|
||||||
// let err = TransportResultErr {
|
}
|
||||||
// error: self
|
|
||||||
// .as_ref()
|
|
||||||
// .map_err(|e| CapturedError::from_display(e.to_string())),
|
|
||||||
// };
|
|
||||||
|
|
||||||
// let mut serialized = Vec::new();
|
impl Transportable<TransportCapturedError> for CapturedError {
|
||||||
// ciborium::into_writer(&err, &mut serialized).unwrap();
|
fn transport_to_bytes(&self) -> Vec<u8> {
|
||||||
// serialized
|
let err = TransportError {
|
||||||
// }
|
error: self.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
// fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
|
let mut serialized = Vec::new();
|
||||||
// where
|
ciborium::into_writer(&err, &mut serialized).unwrap();
|
||||||
// Self: Sized,
|
serialized
|
||||||
// {
|
}
|
||||||
// let err: TransportResultErr<T> = ciborium::from_reader(Cursor::new(bytes))?;
|
|
||||||
// match err.error {
|
|
||||||
// Ok(value) => Ok(Ok(value)),
|
|
||||||
// Err(captured) => Ok(Err(dioxus_core::Error::msg(captured.to_string()))),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[doc(hidden)]
|
fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
|
||||||
// pub struct TransportCapturedError;
|
where
|
||||||
// #[derive(Serialize, Deserialize)]
|
Self: Sized,
|
||||||
// struct TransportError {
|
{
|
||||||
// error: String,
|
let err: TransportError = ciborium::from_reader(Cursor::new(bytes))?;
|
||||||
// }
|
Ok(CapturedError(Arc::new(dioxus_core::Error::msg::<String>(
|
||||||
// impl Transportable<TransportCapturedError> for CapturedError {
|
err.error,
|
||||||
// fn transport_to_bytes(&self) -> Vec<u8> {
|
))))
|
||||||
// let err = TransportError {
|
}
|
||||||
// error: self.to_string(),
|
}
|
||||||
// };
|
|
||||||
|
|
||||||
// let mut serialized = Vec::new();
|
|
||||||
// ciborium::into_writer(&err, &mut serialized).unwrap();
|
|
||||||
// serialized
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
|
|
||||||
// where
|
|
||||||
// Self: Sized,
|
|
||||||
// {
|
|
||||||
// let err: TransportError = ciborium::from_reader(Cursor::new(bytes))?;
|
|
||||||
// Ok(CapturedError(Arc::new(Error::msg::<String>(err.error))))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -88,9 +88,9 @@ use syn::{
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn server(_attr: proc_macro::TokenStream, mut item: TokenStream) -> TokenStream {
|
pub fn server(attr: proc_macro::TokenStream, mut item: TokenStream) -> TokenStream {
|
||||||
// Parse the attribute list using the old server_fn arg parser.
|
// Parse the attribute list using the old server_fn arg parser.
|
||||||
let args = match syn::parse::<ServerFnArgs>(_attr) {
|
let args = match syn::parse::<ServerFnArgs>(attr) {
|
||||||
Ok(args) => args,
|
Ok(args) => args,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let err: TokenStream = err.to_compile_error().into();
|
let err: TokenStream = err.to_compile_error().into();
|
||||||
@@ -202,7 +202,30 @@ fn route_impl_with_route(
|
|||||||
method_from_macro: Option<Method>,
|
method_from_macro: Option<Method>,
|
||||||
) -> syn::Result<TokenStream2> {
|
) -> syn::Result<TokenStream2> {
|
||||||
// Parse the route and function
|
// Parse the route and function
|
||||||
let function = syn::parse::<ItemFn>(item)?;
|
let mut function = syn::parse::<ItemFn>(item)?;
|
||||||
|
|
||||||
|
let middleware_attrs = function
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.filter(|attr| attr.path().is_ident("middleware"))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let middleware_inits = middleware_attrs
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| match f.meta {
|
||||||
|
Meta::List(meta_list) => Ok(meta_list.tokens),
|
||||||
|
_ => Err(Error::new(
|
||||||
|
f.span(),
|
||||||
|
"Expected middleware attribute to be a list, e.g. #[middleware(MyLayer::new())]",
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
// don't re-emit the middleware attribute on the inner
|
||||||
|
function
|
||||||
|
.attrs
|
||||||
|
.retain(|attr| !attr.path().is_ident("middleware"));
|
||||||
|
|
||||||
let server_args = route.server_args.clone();
|
let server_args = route.server_args.clone();
|
||||||
let mut function_on_server = function.clone();
|
let mut function_on_server = function.clone();
|
||||||
@@ -259,15 +282,7 @@ fn route_impl_with_route(
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|attr| attr.path().is_ident("doc"));
|
.filter(|attr| attr.path().is_ident("doc"));
|
||||||
|
|
||||||
let __axum = quote! { dioxus_fullstack::axum };
|
let __axum = quote! { dioxus_server::axum };
|
||||||
|
|
||||||
let (aide_ident_docs, _inner_fn_call, _method_router_ty) = {
|
|
||||||
(
|
|
||||||
quote!(),
|
|
||||||
quote! { #__axum::routing::#http_method(__inner__function__ #ty_generics) },
|
|
||||||
quote! { #__axum::routing::MethodRouter },
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let output_type = match &function.sig.output {
|
let output_type = match &function.sig.output {
|
||||||
syn::ReturnType::Default => parse_quote! { () },
|
syn::ReturnType::Default => parse_quote! { () },
|
||||||
@@ -346,7 +361,7 @@ fn route_impl_with_route(
|
|||||||
quote! {}
|
quote! {}
|
||||||
} else {
|
} else {
|
||||||
quote! {
|
quote! {
|
||||||
let (#(#server_names,)*) = dioxus_fullstack::StreamingContext::extract::<(#(#server_types,)*), _>().await?;
|
let (#(#server_names,)*) = dioxus_fullstack::FullstackContext::extract::<(#(#server_types,)*), _>().await?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -418,9 +433,40 @@ fn route_impl_with_route(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let middleware_extra = middleware_inits
|
||||||
|
.iter()
|
||||||
|
.map(|init| {
|
||||||
|
quote! {
|
||||||
|
.layer(#init)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
#(#fn_docs)*
|
#(#fn_docs)*
|
||||||
#route_docs
|
#route_docs
|
||||||
|
#[deny(
|
||||||
|
unexpected_cfgs,
|
||||||
|
reason = "
|
||||||
|
==========================================================================================
|
||||||
|
Using Dioxus Server Functions requires a `server` feature flag in your `Cargo.toml`.
|
||||||
|
Please add the following to your `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[features]
|
||||||
|
server = [\"dioxus/server\"]
|
||||||
|
```
|
||||||
|
|
||||||
|
To enable better Rust-Analyzer support, you can make `server` a default feature:
|
||||||
|
```toml
|
||||||
|
[features]
|
||||||
|
default = [\"web\", \"server\"]
|
||||||
|
web = [\"dioxus/web\"]
|
||||||
|
server = [\"dioxus/server\"]
|
||||||
|
```
|
||||||
|
==========================================================================================
|
||||||
|
"
|
||||||
|
)]
|
||||||
#vis async fn #fn_name #impl_generics(
|
#vis async fn #fn_name #impl_generics(
|
||||||
#original_inputs
|
#original_inputs
|
||||||
) -> #out_ty #where_clause {
|
) -> #out_ty #where_clause {
|
||||||
@@ -483,7 +529,6 @@ fn route_impl_with_route(
|
|||||||
#function_on_server
|
#function_on_server
|
||||||
|
|
||||||
#[allow(clippy::unused_unit)]
|
#[allow(clippy::unused_unit)]
|
||||||
#aide_ident_docs
|
|
||||||
#asyncness fn __inner__function__ #impl_generics(
|
#asyncness fn __inner__function__ #impl_generics(
|
||||||
___state: #__axum::extract::State<DioxusServerState>,
|
___state: #__axum::extract::State<DioxusServerState>,
|
||||||
#path_extractor
|
#path_extractor
|
||||||
@@ -504,11 +549,13 @@ fn route_impl_with_route(
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
dioxus_fullstack::inventory::submit! {
|
dioxus_server::inventory::submit! {
|
||||||
ServerFunction::new(
|
ServerFunction::new(
|
||||||
dioxus_fullstack::http::Method::#method_ident,
|
dioxus_server::http::Method::#method_ident,
|
||||||
__ENDPOINT_PATH,
|
__ENDPOINT_PATH,
|
||||||
|| #__axum::routing::#http_method(__inner__function__ #ty_generics)
|
|| {
|
||||||
|
#__axum::routing::#http_method(__inner__function__ #ty_generics) #(#middleware_extra)*
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,14 +735,14 @@ impl CompiledRoute {
|
|||||||
let idents = path_iter.clone().map(|item| item.0);
|
let idents = path_iter.clone().map(|item| item.0);
|
||||||
let types = path_iter.clone().map(|item| item.1);
|
let types = path_iter.clone().map(|item| item.1);
|
||||||
quote! {
|
quote! {
|
||||||
dioxus_fullstack::axum::extract::Path((#(#idents,)*)): dioxus_fullstack::axum::extract::Path<(#(#types,)*)>,
|
dioxus_server::axum::extract::Path((#(#idents,)*)): dioxus_server::axum::extract::Path<(#(#types,)*)>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn query_extractor(&self) -> TokenStream2 {
|
pub fn query_extractor(&self) -> TokenStream2 {
|
||||||
let idents = self.query_params.iter().map(|item| &item.0);
|
let idents = self.query_params.iter().map(|item| &item.0);
|
||||||
quote! {
|
quote! {
|
||||||
dioxus_fullstack::axum::extract::Query(__QueryParams__ { #(#idents,)* }): dioxus_fullstack::axum::extract::Query<__QueryParams__>,
|
dioxus_server::axum::extract::Query(__QueryParams__ { #(#idents,)* }): dioxus_server::axum::extract::Query<__QueryParams__>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ default-tls = []
|
|||||||
rustls = ["dep:rustls", "dep:hyper-rustls"]
|
rustls = ["dep:rustls", "dep:hyper-rustls"]
|
||||||
axum-no-default = []
|
axum-no-default = []
|
||||||
rkyv = ["dep:rkyv"]
|
rkyv = ["dep:rkyv"]
|
||||||
|
server = []
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ use dioxus::prelude::*;
|
|||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
use dioxus_server::DioxusRouterExt;
|
||||||
|
|
||||||
// Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address
|
// Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address
|
||||||
// and we use the generated address the CLI gives us
|
// and we use the generated address the CLI gives us
|
||||||
let address = dioxus::cli_config::fullstack_address_or_localhost();
|
let address = dioxus::cli_config::fullstack_address_or_localhost();
|
||||||
@@ -87,7 +89,7 @@ async fn main() {
|
|||||||
let router = axum::Router::new()
|
let router = axum::Router::new()
|
||||||
// You can add a dioxus application to the router with the `serve_dioxus_application` method
|
// You can add a dioxus application to the router with the `serve_dioxus_application` method
|
||||||
// This will add a fallback route to the router that will serve your component and server functions
|
// This will add a fallback route to the router that will serve your component and server functions
|
||||||
.serve_dioxus_application(dioxus::server::ServeConfig::new().unwrap(), App);
|
.serve_dioxus_application(dioxus_server::ServeConfig::new().unwrap(), App);
|
||||||
|
|
||||||
// Finally, we can launch the server
|
// Finally, we can launch the server
|
||||||
let router = router.into_make_service();
|
let router = router.into_make_service();
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ use std::sync::Arc;
|
|||||||
use crate::IncrementalRendererConfig;
|
use crate::IncrementalRendererConfig;
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub(crate) type ContextProviders =
|
pub(crate) type ContextProviders = Arc<Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync + 'static>>>;
|
||||||
Arc<Vec<Box<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync + 'static>>>;
|
|
||||||
|
|
||||||
/// A ServeConfig is used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus_ssr`].
|
/// A ServeConfig is used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus_ssr`].
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
@@ -21,8 +20,7 @@ pub struct ServeConfigBuilder {
|
|||||||
pub(crate) index_html: Option<String>,
|
pub(crate) index_html: Option<String>,
|
||||||
pub(crate) index_path: Option<PathBuf>,
|
pub(crate) index_path: Option<PathBuf>,
|
||||||
pub(crate) incremental: Option<IncrementalRendererConfig>,
|
pub(crate) incremental: Option<IncrementalRendererConfig>,
|
||||||
pub(crate) context_providers:
|
pub(crate) context_providers: Vec<Arc<dyn Fn() -> Box<dyn Any> + Send + Sync + 'static>>,
|
||||||
Vec<Arc<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync + 'static>>,
|
|
||||||
pub(crate) streaming_mode: StreamingMode,
|
pub(crate) streaming_mode: StreamingMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,31 +128,24 @@ impl ServeConfigBuilder {
|
|||||||
/// use std::any::Any;
|
/// use std::any::Any;
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// fn main() {
|
||||||
/// #[cfg(feature = "web")]
|
|
||||||
/// // Hydrate the application on the client
|
/// // Hydrate the application on the client
|
||||||
|
/// #[cfg(not(feature = "server"))]
|
||||||
/// dioxus::launch(app);
|
/// dioxus::launch(app);
|
||||||
|
///
|
||||||
/// #[cfg(feature = "server")]
|
/// #[cfg(feature = "server")]
|
||||||
/// {
|
/// dioxus_server::serve(|| async move {
|
||||||
/// tokio::runtime::Runtime::new()
|
/// use dioxus_server::{axum, ServeConfigBuilder, DioxusRouterExt};
|
||||||
/// .unwrap()
|
///
|
||||||
/// .block_on(async move {
|
|
||||||
/// let address = dioxus_cli_config::fullstack_address_or_localhost();
|
|
||||||
/// let listener = tokio::net::TcpListener::bind(address)
|
|
||||||
/// .await
|
|
||||||
/// .unwrap();
|
|
||||||
/// let config = ServeConfigBuilder::default()
|
/// let config = ServeConfigBuilder::default()
|
||||||
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
|
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
|
||||||
/// .context_providers(Arc::new(vec![Box::new(|| Box::new(1234u32) as Box<dyn Any>) as Box<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync>]));
|
/// .context_providers(Arc::new(vec![Box::new(|| Box::new(1234u32) as Box<dyn Any>) as Box<dyn Fn() -> Box<dyn Any> + Send + Sync>]))
|
||||||
/// axum::serve(
|
/// .build()?;
|
||||||
/// listener,
|
///
|
||||||
|
/// Ok(
|
||||||
/// axum::Router::new()
|
/// axum::Router::new()
|
||||||
/// .serve_dioxus_application(config, app)
|
/// .serve_dioxus_application(config, app)
|
||||||
/// .into_make_service(),
|
|
||||||
/// )
|
/// )
|
||||||
/// .await
|
/// })
|
||||||
/// .unwrap();
|
|
||||||
/// });
|
|
||||||
/// }
|
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// #[server]
|
/// #[server]
|
||||||
@@ -195,32 +186,25 @@ impl ServeConfigBuilder {
|
|||||||
/// use dioxus::prelude::*;
|
/// use dioxus::prelude::*;
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// fn main() {
|
||||||
/// #[cfg(feature = "web")]
|
/// #[cfg(not(feature = "server"))]
|
||||||
/// // Hydrate the application on the client
|
/// // Hydrate the application on the client
|
||||||
/// dioxus::launch(app);
|
/// dioxus::launch(app);
|
||||||
|
///
|
||||||
/// #[cfg(feature = "server")]
|
/// #[cfg(feature = "server")]
|
||||||
/// {
|
/// dioxus_server::serve(|| async move {
|
||||||
/// tokio::runtime::Runtime::new()
|
/// use dioxus_server::{axum, ServeConfigBuilder, DioxusRouterExt};
|
||||||
/// .unwrap()
|
///
|
||||||
/// .block_on(async move {
|
|
||||||
/// let address = dioxus_cli_config::fullstack_address_or_localhost();
|
|
||||||
/// let listener = tokio::net::TcpListener::bind(address)
|
|
||||||
/// .await
|
|
||||||
/// .unwrap();
|
|
||||||
/// let config = ServeConfigBuilder::default()
|
/// let config = ServeConfigBuilder::default()
|
||||||
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
|
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
|
||||||
/// .context_provider(move || 1234u32);
|
/// .context_provider(|| 1234u32)
|
||||||
/// axum::serve(
|
/// .build()?;
|
||||||
/// listener,
|
///
|
||||||
|
/// Ok(
|
||||||
/// axum::Router::new()
|
/// axum::Router::new()
|
||||||
/// .serve_dioxus_application(config, app)
|
/// .serve_dioxus_application(config, app)
|
||||||
/// .into_make_service(),
|
|
||||||
/// )
|
/// )
|
||||||
/// .await
|
|
||||||
/// .unwrap();
|
|
||||||
/// });
|
/// });
|
||||||
/// }
|
/// }
|
||||||
/// }
|
|
||||||
///
|
///
|
||||||
/// #[server]
|
/// #[server]
|
||||||
/// async fn read_context() -> ServerFnResult<u32> {
|
/// async fn read_context() -> ServerFnResult<u32> {
|
||||||
@@ -252,32 +236,25 @@ impl ServeConfigBuilder {
|
|||||||
/// use dioxus::prelude::*;
|
/// use dioxus::prelude::*;
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// fn main() {
|
||||||
/// #[cfg(feature = "web")]
|
|
||||||
/// // Hydrate the application on the client
|
/// // Hydrate the application on the client
|
||||||
|
/// #[cfg(not(feature = "server"))]
|
||||||
/// dioxus::launch(app);
|
/// dioxus::launch(app);
|
||||||
|
///
|
||||||
|
/// // Run a custom server with axum on the server
|
||||||
/// #[cfg(feature = "server")]
|
/// #[cfg(feature = "server")]
|
||||||
/// {
|
/// dioxus_server::serve(|| async move {
|
||||||
/// tokio::runtime::Runtime::new()
|
/// use dioxus_server::{axum, ServeConfigBuilder, DioxusRouterExt};
|
||||||
/// .unwrap()
|
///
|
||||||
/// .block_on(async move {
|
|
||||||
/// let address = dioxus_cli_config::fullstack_address_or_localhost();
|
|
||||||
/// let listener = tokio::net::TcpListener::bind(address)
|
|
||||||
/// .await
|
|
||||||
/// .unwrap();
|
|
||||||
/// let config = ServeConfigBuilder::default()
|
/// let config = ServeConfigBuilder::default()
|
||||||
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
|
/// .context(1234u32)
|
||||||
/// .context(1234u32);
|
/// .build()?;
|
||||||
/// axum::serve(
|
//
|
||||||
/// listener,
|
/// Ok(
|
||||||
/// axum::Router::new()
|
/// axum::Router::new()
|
||||||
/// .serve_dioxus_application(config, app)
|
/// .serve_dioxus_application(config, app)
|
||||||
/// .into_make_service(),
|
|
||||||
/// )
|
/// )
|
||||||
/// .await
|
|
||||||
/// .unwrap();
|
|
||||||
/// });
|
/// });
|
||||||
/// }
|
/// }
|
||||||
/// }
|
|
||||||
///
|
///
|
||||||
/// #[server]
|
/// #[server]
|
||||||
/// async fn read_context() -> ServerFnResult<u32> {
|
/// async fn read_context() -> ServerFnResult<u32> {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ pub(crate) use config::*;
|
|||||||
pub use config::*;
|
pub use config::*;
|
||||||
pub use config::{ServeConfig, ServeConfigBuilder};
|
pub use config::{ServeConfig, ServeConfigBuilder};
|
||||||
pub use document::ServerDocument;
|
pub use document::ServerDocument;
|
||||||
|
pub use http;
|
||||||
|
pub use inventory;
|
||||||
pub use server::*;
|
pub use server::*;
|
||||||
|
|
||||||
pub mod redirect;
|
pub mod redirect;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use axum::{
|
|||||||
routing::*,
|
routing::*,
|
||||||
};
|
};
|
||||||
use dioxus_core::{Element, VirtualDom};
|
use dioxus_core::{Element, VirtualDom};
|
||||||
|
use dioxus_fullstack_core::HttpError;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use http::{header::*, request::Parts};
|
use http::{header::*, request::Parts};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -299,10 +300,15 @@ impl RenderHandleState {
|
|||||||
let (parts, _) = request.into_parts();
|
let (parts, _) = request.into_parts();
|
||||||
|
|
||||||
match state.render(parts, cfg, build_virtual_dom).await {
|
match state.render(parts, cfg, build_virtual_dom).await {
|
||||||
Ok((freshness, rx)) => {
|
Ok((status, freshness, rx)) => {
|
||||||
let mut response =
|
let mut response = axum::response::Response::builder()
|
||||||
axum::response::Html::from(Body::from_stream(rx)).into_response();
|
.status(status.status)
|
||||||
|
.header(CONTENT_TYPE, "text/html; charset=utf-8")
|
||||||
|
.body(Body::from_stream(rx))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
freshness.write(response.headers_mut());
|
freshness.write(response.headers_mut());
|
||||||
|
|
||||||
Result::<http::Response<axum::body::Body>, StatusCode>::Ok(response)
|
Result::<http::Response<axum::body::Body>, StatusCode>::Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,6 +344,7 @@ impl RenderHandleState {
|
|||||||
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
(
|
(
|
||||||
|
HttpError,
|
||||||
RenderFreshness,
|
RenderFreshness,
|
||||||
impl Stream<Item = Result<String, IncrementalRendererError>>,
|
impl Stream<Item = Result<String, IncrementalRendererError>>,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::handler::Handler;
|
use axum::handler::Handler;
|
||||||
use axum::routing::MethodRouter;
|
use axum::routing::MethodRouter;
|
||||||
use axum::Router; // both req/res // both req/res // req only
|
use axum::Router;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use dioxus_fullstack_core::DioxusServerState;
|
use dioxus_fullstack_core::DioxusServerState;
|
||||||
use http::Method;
|
use http::Method;
|
||||||
@@ -111,20 +111,6 @@ impl ServerFunction {
|
|||||||
// .map(|v| v.contains("text/html"))
|
// .map(|v| v.contains("text/html"))
|
||||||
// .unwrap_or(false);
|
// .unwrap_or(false);
|
||||||
|
|
||||||
// let mthd: MethodRouter<()> = axum::routing::get(self.handler);
|
|
||||||
|
|
||||||
// // this is taken from server_fn source...
|
|
||||||
// //
|
|
||||||
// // [`server_fn::axum::get_server_fn_service`]
|
|
||||||
// let mut service = {
|
|
||||||
// let middleware = self.middleware();
|
|
||||||
// let mut service = self.clone().boxed();
|
|
||||||
// for middleware in middleware {
|
|
||||||
// service = middleware.layer(service);
|
|
||||||
// }
|
|
||||||
// service
|
|
||||||
// };
|
|
||||||
|
|
||||||
// actually run the server fn (which may use the server context)
|
// actually run the server fn (which may use the server context)
|
||||||
// let fut = crate::with_server_context(server_context.clone(), || service.run(req));
|
// let fut = crate::with_server_context(server_context.clone(), || service.run(req));
|
||||||
// let fut = crate::with_server_context(server_context.clone(), || service.run(req));
|
// let fut = crate::with_server_context(server_context.clone(), || service.run(req));
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ use dioxus_core::{
|
|||||||
SuspenseContext, TemplateNode, VNode, VirtualDom,
|
SuspenseContext, TemplateNode, VNode, VirtualDom,
|
||||||
};
|
};
|
||||||
use dioxus_fullstack_core::{history::provide_fullstack_history_context, HttpError, ServerFnError};
|
use dioxus_fullstack_core::{history::provide_fullstack_history_context, HttpError, ServerFnError};
|
||||||
|
use dioxus_fullstack_core::{FullstackContext, StreamingStatus};
|
||||||
use dioxus_fullstack_core::{HydrationContext, SerializedHydrationData};
|
use dioxus_fullstack_core::{HydrationContext, SerializedHydrationData};
|
||||||
use dioxus_fullstack_core::{StreamingContext, StreamingStatus};
|
|
||||||
use dioxus_router::ParseRouteError;
|
use dioxus_router::ParseRouteError;
|
||||||
use dioxus_ssr::Renderer;
|
use dioxus_ssr::Renderer;
|
||||||
use futures_channel::mpsc::Sender;
|
use futures_channel::mpsc::Sender;
|
||||||
@@ -67,7 +67,8 @@ impl SsrRendererPool {
|
|||||||
route: &str,
|
route: &str,
|
||||||
render_into: &mut Sender<Result<String, IncrementalRendererError>>,
|
render_into: &mut Sender<Result<String, IncrementalRendererError>>,
|
||||||
) -> Option<RenderFreshness> {
|
) -> Option<RenderFreshness> {
|
||||||
if let Some(incremental) = &self.incremental_cache {
|
let incremental = self.incremental_cache.as_ref()?;
|
||||||
|
|
||||||
if let Ok(mut incremental) = incremental.write() {
|
if let Ok(mut incremental) = incremental.write() {
|
||||||
match incremental.get(route) {
|
match incremental.get(route) {
|
||||||
Ok(Some(cached_render)) => {
|
Ok(Some(cached_render)) => {
|
||||||
@@ -83,14 +84,12 @@ impl SsrRendererPool {
|
|||||||
return Some(freshness);
|
return Some(freshness);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(
|
tracing::error!("Failed to get route \"{route}\" from incremental cache: {e}");
|
||||||
"Failed to get route \"{route}\" from incremental cache: {e}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +102,7 @@ impl SsrRendererPool {
|
|||||||
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
(
|
(
|
||||||
|
HttpError,
|
||||||
RenderFreshness,
|
RenderFreshness,
|
||||||
impl Stream<Item = Result<String, IncrementalRendererError>>,
|
impl Stream<Item = Result<String, IncrementalRendererError>>,
|
||||||
),
|
),
|
||||||
@@ -150,6 +150,10 @@ impl SsrRendererPool {
|
|||||||
// before we even spawn anything, we can check synchronously if we have the route cached
|
// before we even spawn anything, we can check synchronously if we have the route cached
|
||||||
if let Some(freshness) = self.check_cached_route(&route, &mut into) {
|
if let Some(freshness) = self.check_cached_route(&route, &mut into) {
|
||||||
return Ok((
|
return Ok((
|
||||||
|
HttpError {
|
||||||
|
status: StatusCode::OK,
|
||||||
|
message: None,
|
||||||
|
},
|
||||||
freshness,
|
freshness,
|
||||||
ReceiverWithDrop {
|
ReceiverWithDrop {
|
||||||
receiver: rx,
|
receiver: rx,
|
||||||
@@ -187,13 +191,14 @@ impl SsrRendererPool {
|
|||||||
|
|
||||||
// Provide the document and streaming context to the root of the app
|
// Provide the document and streaming context to the root of the app
|
||||||
let streaming_context =
|
let streaming_context =
|
||||||
virtual_dom.in_scope(ScopeId::ROOT, || StreamingContext::new(parts));
|
virtual_dom.in_scope(ScopeId::ROOT, || FullstackContext::new(parts));
|
||||||
virtual_dom.provide_root_context(document.clone() as Rc<dyn dioxus_document::Document>);
|
virtual_dom.provide_root_context(document.clone() as Rc<dyn dioxus_document::Document>);
|
||||||
virtual_dom.provide_root_context(streaming_context.clone());
|
virtual_dom.provide_root_context(streaming_context.clone());
|
||||||
|
|
||||||
virtual_dom.in_scope(ScopeId::ROOT, || {
|
virtual_dom.in_scope(ScopeId::ROOT, || {
|
||||||
// Wrap the memory history in a fullstack history provider to provide the initial route for hydration
|
// Wrap the memory history in a fullstack history provider to provide the initial route for hydration
|
||||||
provide_fullstack_history_context(history);
|
provide_fullstack_history_context(history);
|
||||||
|
|
||||||
// Provide a hydration compatible error boundary that serializes errors for the client
|
// Provide a hydration compatible error boundary that serializes errors for the client
|
||||||
dioxus_core::provide_create_error_boundary(
|
dioxus_core::provide_create_error_boundary(
|
||||||
dioxus_fullstack_core::init_error_boundary,
|
dioxus_fullstack_core::init_error_boundary,
|
||||||
@@ -203,6 +208,10 @@ impl SsrRendererPool {
|
|||||||
// rebuild the virtual dom
|
// rebuild the virtual dom
|
||||||
virtual_dom.rebuild_in_place();
|
virtual_dom.rebuild_in_place();
|
||||||
|
|
||||||
|
// Process any pending events that might have been triggered during the rebuild
|
||||||
|
// This ensures things like error boundaries get a chance to run.
|
||||||
|
virtual_dom.process_events();
|
||||||
|
|
||||||
// If streaming is disabled, wait for the virtual dom to finish all suspense work
|
// If streaming is disabled, wait for the virtual dom to finish all suspense work
|
||||||
// before rendering anything
|
// before rendering anything
|
||||||
if streaming_mode == StreamingMode::Disabled {
|
if streaming_mode == StreamingMode::Disabled {
|
||||||
@@ -212,8 +221,9 @@ impl SsrRendererPool {
|
|||||||
loop {
|
loop {
|
||||||
// Check if the router has finished and set the streaming context to finished
|
// Check if the router has finished and set the streaming context to finished
|
||||||
let streaming_context_finished = virtual_dom
|
let streaming_context_finished = virtual_dom
|
||||||
.in_scope(ScopeId::ROOT, || streaming_context.current_status())
|
.in_scope(ScopeId::ROOT, || streaming_context.streaming_state())
|
||||||
== StreamingStatus::InitialChunkCommitted;
|
== StreamingStatus::InitialChunkCommitted;
|
||||||
|
|
||||||
// Or if this app isn't using the router and has finished suspense
|
// Or if this app isn't using the router and has finished suspense
|
||||||
let suspense_finished = !virtual_dom.suspended_tasks_remaining();
|
let suspense_finished = !virtual_dom.suspended_tasks_remaining();
|
||||||
if streaming_context_finished || suspense_finished {
|
if streaming_context_finished || suspense_finished {
|
||||||
@@ -228,7 +238,7 @@ impl SsrRendererPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if there are any errors
|
// check if there are any errors from the root error boundary
|
||||||
let error = virtual_dom.in_scope(ScopeId::ROOT_ERROR_BOUNDARY, || {
|
let error = virtual_dom.in_scope(ScopeId::ROOT_ERROR_BOUNDARY, || {
|
||||||
consume_context::<ErrorContext>().error()
|
consume_context::<ErrorContext>().error()
|
||||||
});
|
});
|
||||||
@@ -287,8 +297,11 @@ impl SsrRendererPool {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check the FullstackContext in case the user set the statuscode manually or via a layout.
|
||||||
|
let http_status = streaming_context.current_http_status();
|
||||||
|
|
||||||
// Now that we handled any errors from rendering, we can send the initial ok result
|
// Now that we handled any errors from rendering, we can send the initial ok result
|
||||||
_ = initial_result_tx.send(Ok(()));
|
_ = initial_result_tx.send(Ok(http_status));
|
||||||
|
|
||||||
// Wait long enough to assemble the `<head>` of the document before starting to stream
|
// Wait long enough to assemble the `<head>` of the document before starting to stream
|
||||||
let mut pre_body = String::new();
|
let mut pre_body = String::new();
|
||||||
@@ -412,11 +425,12 @@ impl SsrRendererPool {
|
|||||||
let join_handle = Self::spawn_platform(create_render_future);
|
let join_handle = Self::spawn_platform(create_render_future);
|
||||||
|
|
||||||
// Wait for the initial result which determines the status code
|
// Wait for the initial result which determines the status code
|
||||||
initial_result_rx
|
let status = initial_result_rx
|
||||||
.await
|
.await
|
||||||
.map_err(|err| SSRError::Incremental(IncrementalRendererError::Other(err.into())))??;
|
.map_err(|err| SSRError::Incremental(IncrementalRendererError::Other(err.into())))??;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
|
status,
|
||||||
RenderFreshness::now(None),
|
RenderFreshness::now(None),
|
||||||
ReceiverWithDrop {
|
ReceiverWithDrop {
|
||||||
receiver: rx,
|
receiver: rx,
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ mod client;
|
|||||||
pub use client::*;
|
pub use client::*;
|
||||||
|
|
||||||
pub use axum::extract::Json;
|
pub use axum::extract::Json;
|
||||||
|
pub use axum::response::{NoContent, Redirect};
|
||||||
|
|
||||||
pub use payloads::*;
|
pub use payloads::*;
|
||||||
pub mod payloads {
|
pub mod payloads {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{ClientResponse, FromResponse};
|
use crate::{ClientResponse, FromResponse};
|
||||||
pub use axum::extract::Json;
|
pub use axum::extract::Json;
|
||||||
use axum::response::Html;
|
use axum::response::{Html, NoContent, Redirect};
|
||||||
use dioxus_fullstack_core::{RequestError, ServerFnError};
|
use dioxus_fullstack_core::{RequestError, ServerFnError};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
use http::StatusCode;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
impl<T: From<String>> FromResponse for Html<T> {
|
impl<T: From<String>> FromResponse for Html<T> {
|
||||||
@@ -33,6 +34,39 @@ impl<T: DeserializeOwned> FromResponse for Json<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromResponse for Redirect {
|
||||||
|
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
|
||||||
|
async move {
|
||||||
|
let location = res
|
||||||
|
.headers()
|
||||||
|
.get(http::header::LOCATION)
|
||||||
|
.ok_or_else(|| RequestError::Redirect("Missing Location header".into()))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| RequestError::Redirect("Invalid Location header".into()))?;
|
||||||
|
match res.status() {
|
||||||
|
StatusCode::SEE_OTHER => Ok(Redirect::to(location)),
|
||||||
|
StatusCode::TEMPORARY_REDIRECT => Ok(Redirect::temporary(location)),
|
||||||
|
StatusCode::PERMANENT_REDIRECT => Ok(Redirect::permanent(location)),
|
||||||
|
_ => Err(RequestError::Redirect("Not a redirect status code".into()).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromResponse for NoContent {
|
||||||
|
fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
|
||||||
|
async move {
|
||||||
|
let status = res.status();
|
||||||
|
if status == StatusCode::NO_CONTENT {
|
||||||
|
Ok(NoContent)
|
||||||
|
} else {
|
||||||
|
let body = res.text().await.unwrap_or_else(|_| "".into());
|
||||||
|
Err(RequestError::Status(body, status.into()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Implementation of `FromResponse` for `axum::response::Response`.
|
/// Implementation of `FromResponse` for `axum::response::Response`.
|
||||||
///
|
///
|
||||||
/// This allows converting a `ClientResponse` (from a client-side HTTP request)
|
/// This allows converting a `ClientResponse` (from a client-side HTTP request)
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ class IPC {
|
|||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
// we ping every 30 seconds to keep the websocket alive
|
// we ping every 30 seconds to keep the websocket alive
|
||||||
setInterval(ping, 30000);
|
setInterval(ping, 30000);
|
||||||
ws.send(window.interpreter.serializeIpcMessage("initialize"));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (err) => {
|
ws.onerror = (err) => {
|
||||||
@@ -40,7 +39,6 @@ class IPC {
|
|||||||
window.interpreter.run_from_bytes(messageData);
|
window.interpreter.run_from_bytes(messageData);
|
||||||
} else {
|
} else {
|
||||||
// text frame
|
// text frame
|
||||||
|
|
||||||
let decoder = new TextDecoder("utf-8");
|
let decoder = new TextDecoder("utf-8");
|
||||||
|
|
||||||
// Using decode method to get string output
|
// Using decode method to get string output
|
||||||
|
|||||||
@@ -274,6 +274,7 @@ pub enum QueryError {
|
|||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub(crate) struct QueryResult {
|
pub(crate) struct QueryResult {
|
||||||
id: usize,
|
id: usize,
|
||||||
|
#[serde(default)]
|
||||||
data: Value,
|
data: Value,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
returned_value: bool,
|
returned_value: bool,
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// @ts-check
|
||||||
|
const { test, expect } = require("@playwright/test");
|
||||||
|
|
||||||
|
test("errors", async ({ page }) => {
|
||||||
|
// Make sure going to home returns a 200
|
||||||
|
const res = await page.goto("http://localhost:8124");
|
||||||
|
if (!res) {
|
||||||
|
throw new Error("Failed to navigate to http://localhost:8124");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
|
||||||
|
// Go to /blog/1 which should also be okay
|
||||||
|
const blogRes = await page.goto("http://localhost:8124/blog/1");
|
||||||
|
if (!blogRes) {
|
||||||
|
throw new Error("Failed to navigate to http://localhost:8124/blog/1");
|
||||||
|
}
|
||||||
|
expect(blogRes.status()).toBe(200);
|
||||||
|
|
||||||
|
// Go to /blog/2 which should be a 201
|
||||||
|
const blogErrorRes = await page.goto("http://localhost:8124/blog/2");
|
||||||
|
if (!blogErrorRes) {
|
||||||
|
throw new Error("Failed to navigate to http://localhost:8124/blog/2");
|
||||||
|
}
|
||||||
|
expect(blogErrorRes.status()).toBe(201);
|
||||||
|
|
||||||
|
// Go to /blog/3 which should be a 500
|
||||||
|
const blogPanicRes = await page.goto("http://localhost:8124/blog/3");
|
||||||
|
if (!blogPanicRes) {
|
||||||
|
throw new Error("Failed to navigate to http://localhost:8124/blog/3");
|
||||||
|
}
|
||||||
|
expect(blogPanicRes.status()).toBe(500);
|
||||||
|
|
||||||
|
// Go to /blog/4 which should be a 404
|
||||||
|
const blogNotFoundRes = await page.goto("http://localhost:8124/blog/4");
|
||||||
|
if (!blogNotFoundRes) {
|
||||||
|
throw new Error("Failed to navigate to http://localhost:8124/blog/4");
|
||||||
|
}
|
||||||
|
expect(blogNotFoundRes.status()).toBe(404);
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.dioxus
|
||||||
|
dist
|
||||||
|
target
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "dioxus-playwright-fullstack-error-codes-test"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dioxus = { workspace = true, features = ["fullstack", "router"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["server", "web"]
|
||||||
|
server = ["dioxus/server"]
|
||||||
|
web = ["dioxus/web"]
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
//! To render custom error pages, you can create a layout component that captures errors from routes
|
||||||
|
//! with an `ErrorBoundary` and display different content based on the error type.
|
||||||
|
//!
|
||||||
|
//! While capturing the error, we set the appropriate HTTP status code using `FullstackContext::commit_error_status`.
|
||||||
|
//! The router will then use this status code when doing server-side rendering (SSR).
|
||||||
|
//!
|
||||||
|
//! Any errors not captured by an error boundary will be handled by dioxus-ssr itself, which will render
|
||||||
|
//! a generic error page instead.
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_fullstack::{FullstackContext, StatusCode};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus::launch(|| {
|
||||||
|
rsx! {
|
||||||
|
Router::<Route> {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Routable, PartialEq, Clone, Debug)]
|
||||||
|
enum Route {
|
||||||
|
#[layout(ErrorLayout)]
|
||||||
|
#[route("/")]
|
||||||
|
Home,
|
||||||
|
|
||||||
|
#[route("/blog/:id")]
|
||||||
|
Blog { id: u32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Home() -> Element {
|
||||||
|
rsx! {
|
||||||
|
div { "Welcome to the home page!" }
|
||||||
|
div { display: "flex", flex_direction: "column",
|
||||||
|
Link { to: Route::Blog { id: 1 }, "Go to blog post 1" }
|
||||||
|
Link { to: Route::Blog { id: 2 }, "Go to blog post 2 (201)" }
|
||||||
|
Link { to: Route::Blog { id: 3 }, "Go to blog post 3 (error)" }
|
||||||
|
Link { to: Route::Blog { id: 4 }, "Go to blog post 4 (not found)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Blog(id: u32) -> Element {
|
||||||
|
match id {
|
||||||
|
1 => rsx! { div { "Blog post 1" } },
|
||||||
|
2 => {
|
||||||
|
FullstackContext::commit_http_status(StatusCode::CREATED, None);
|
||||||
|
rsx! { div { "Blog post 2" } }
|
||||||
|
}
|
||||||
|
3 => dioxus_core::bail!("An error occurred while loading the blog post!"),
|
||||||
|
_ => HttpError::not_found("Blog post not found")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// In our `ErrorLayout` component, we wrap the `Outlet` in an `ErrorBoundary`. This lets us attempt
|
||||||
|
/// to downcast the error to an `HttpError` and set the appropriate status code.
|
||||||
|
///
|
||||||
|
/// The `commit_error_status` function will attempt to downcast the error to an `HttpError` and
|
||||||
|
/// set the status code accordingly. Note that you can commit any status code you want with `commit_http_status`.
|
||||||
|
///
|
||||||
|
/// The router will automatically set the HTTP status code when doing SSR.
|
||||||
|
#[component]
|
||||||
|
fn ErrorLayout() -> Element {
|
||||||
|
rsx! {
|
||||||
|
ErrorBoundary {
|
||||||
|
handle_error: move |err: ErrorContext| {
|
||||||
|
let http_error = FullstackContext::commit_error_status(err.error().unwrap());
|
||||||
|
match http_error.status {
|
||||||
|
StatusCode::NOT_FOUND => rsx! { div { "404 - Page not found" } },
|
||||||
|
StatusCode::UNAUTHORIZED => rsx! { div { "401 - Unauthorized" } },
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR => rsx! { div { "500 - Internal Server Error" } },
|
||||||
|
_ => rsx! { div { "An unknown error occurred" } },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Outlet::<Route> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// This test is used by playwright configured in the root of the repo
|
// This test is used by playwright configured in the root of the repo
|
||||||
|
|
||||||
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
|
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
|
||||||
use dioxus::prelude::*;
|
use dioxus::{logger::tracing::Level, prelude::*};
|
||||||
|
|
||||||
fn app() -> Element {
|
fn app() -> Element {
|
||||||
let mut num = use_signal(|| 0);
|
let mut num = use_signal(|| 0);
|
||||||
@@ -40,6 +40,8 @@ fn OnMounted() -> Element {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
_ = dioxus::logger::init(Level::DEBUG);
|
||||||
|
|
||||||
let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
|
let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
|
||||||
|
|
||||||
let view = dioxus_liveview::LiveViewPool::new();
|
let view = dioxus_liveview::LiveViewPool::new();
|
||||||
|
|||||||
@@ -215,5 +215,14 @@ module.exports = defineConfig({
|
|||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
cwd: path.join(process.cwd(), "fullstack-error-codes"),
|
||||||
|
command:
|
||||||
|
'cargo run --package dioxus-cli --release -- run --force-sequential --addr "127.0.0.1" --port 8124',
|
||||||
|
port: 8124,
|
||||||
|
timeout: 50 * 60 * 1000,
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
stdout: "pipe",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user