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:
Jonathan Kelley authored and GitHub committed 2025-10-10 21:43:57 -07:00
1 parent 8a81475ba8
commit 9243ef4128
35 files changed
+935 -800

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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> {}
}
}
}
+55
View File
@@ -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())
}
+50
View File
@@ -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
View File
@@ -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,
}
}
}
+1 -1
View File
@@ -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)
} }
} }
+3 -3
View File
@@ -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();
+3 -1
View File
@@ -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;
+1 -3
View File
@@ -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 {}
+3 -1
View File
@@ -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
+1 -1
View File
@@ -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")]
+17 -51
View File
@@ -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()
// }
// }
+13 -304
View File
@@ -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
// // }
+94 -24
View File
@@ -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
}
+103 -142
View File
@@ -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))))
// }
// }
+66 -19
View File
@@ -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__>,
} }
} }
+1
View File
@@ -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"]
+3 -1
View File
@@ -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();
+48 -71
View File
@@ -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")]
/// tokio::runtime::Runtime::new() /// dioxus_server::serve(|| async move {
/// .unwrap() /// use dioxus_server::{axum, ServeConfigBuilder, DioxusRouterExt};
/// .block_on(async move { ///
/// let address = dioxus_cli_config::fullstack_address_or_localhost(); /// let config = ServeConfigBuilder::default()
/// let listener = tokio::net::TcpListener::bind(address) /// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
/// .await /// .context_providers(Arc::new(vec![Box::new(|| Box::new(1234u32) as Box<dyn Any>) as Box<dyn Fn() -> Box<dyn Any> + Send + Sync>]))
/// .unwrap(); /// .build()?;
/// 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 /// Ok(
/// .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>])); /// axum::Router::new()
/// axum::serve( /// .serve_dioxus_application(config, app)
/// listener, /// )
/// axum::Router::new() /// })
/// .serve_dioxus_application(config, app)
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// });
/// }
/// } /// }
/// ///
/// #[server] /// #[server]
@@ -195,31 +186,24 @@ 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 config = ServeConfigBuilder::default()
/// let address = dioxus_cli_config::fullstack_address_or_localhost(); /// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
/// let listener = tokio::net::TcpListener::bind(address) /// .context_provider(|| 1234u32)
/// .await /// .build()?;
/// .unwrap(); ///
/// let config = ServeConfigBuilder::default() /// Ok(
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder /// axum::Router::new()
/// .context_provider(move || 1234u32); /// .serve_dioxus_application(config, app)
/// axum::serve( /// )
/// listener, /// });
/// axum::Router::new()
/// .serve_dioxus_application(config, app)
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// });
/// }
/// } /// }
/// ///
/// #[server] /// #[server]
@@ -252,31 +236,24 @@ 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 config = ServeConfigBuilder::default()
/// let address = dioxus_cli_config::fullstack_address_or_localhost(); /// .context(1234u32)
/// let listener = tokio::net::TcpListener::bind(address) /// .build()?;
/// .await //
/// .unwrap(); /// Ok(
/// let config = ServeConfigBuilder::default() /// axum::Router::new()
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder /// .serve_dioxus_application(config, app)
/// .context(1234u32); /// )
/// axum::serve( /// });
/// listener,
/// axum::Router::new()
/// .serve_dioxus_application(config, app)
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// });
/// }
/// } /// }
/// ///
/// #[server] /// #[server]
+2
View File
@@ -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;
+10 -3
View File
@@ -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 -15
View File
@@ -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));
+41 -27
View File
@@ -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,30 +67,29 @@ 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() {
match incremental.get(route) { if let Ok(mut incremental) = incremental.write() {
Ok(Some(cached_render)) => { match incremental.get(route) {
let CachedRender { Ok(Some(cached_render)) => {
freshness, let CachedRender {
response, freshness,
.. response,
} = cached_render; ..
_ = render_into.start_send( } = cached_render;
String::from_utf8(response.to_vec()) _ = render_into.start_send(
.map_err(|err| IncrementalRendererError::Other(err.into())), String::from_utf8(response.to_vec())
); .map_err(|err| IncrementalRendererError::Other(err.into())),
return Some(freshness); );
} return Some(freshness);
Err(e) => {
tracing::error!(
"Failed to get route \"{route}\" from incremental cache: {e}"
);
}
_ => {}
} }
Err(e) => {
tracing::error!("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,
+1
View File
@@ -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 {
+35 -1
View File
@@ -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)
-2
View File
@@ -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
+4 -3
View File
@@ -105,14 +105,14 @@ impl QueryEngine {
if (!window.{QUEUE_NAME}) {{ if (!window.{QUEUE_NAME}) {{
window.{QUEUE_NAME} = []; window.{QUEUE_NAME} = [];
}} }}
let _request_id = {request_id}; let _request_id = {request_id};
if (!window.{QUEUE_NAME}[{request_id}]) {{ if (!window.{QUEUE_NAME}[{request_id}]) {{
window.{QUEUE_NAME}[{request_id}] = []; window.{QUEUE_NAME}[{request_id}] = [];
}} }}
let _message_queue = window.{QUEUE_NAME}[{request_id}]; let _message_queue = window.{QUEUE_NAME}[{request_id}];
{script} {script}
}})().then((result)=>{{ }})().then((result)=>{{
let returned_value = {{ let returned_value = {{
@@ -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",
},
], ],
}); });