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

@@ -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
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
use dioxus::prelude::*;
use dioxus::{logger::tracing::Level, prelude::*};
fn app() -> Element {
let mut num = use_signal(|| 0);
@@ -40,6 +40,8 @@ fn OnMounted() -> Element {
#[tokio::main]
async fn main() {
_ = dioxus::logger::init(Level::DEBUG);
let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
let view = dioxus_liveview::LiveViewPool::new();
@@ -215,5 +215,14 @@ module.exports = defineConfig({
reuseExistingServer: !process.env.CI,
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",
},
],
});