Skip to content

Commit 16e9e03

Browse files
authored
Merge branch 'DioxusLabs:main' into main
2 parents e4d1d9a + 09a4b5e commit 16e9e03

File tree

18 files changed

+253
-46
lines changed

18 files changed

+253
-46
lines changed

Cargo.lock

+12-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ members = [
9898
"packages/playwright-tests/web",
9999
"packages/playwright-tests/fullstack",
100100
"packages/playwright-tests/fullstack-mounted",
101+
"packages/playwright-tests/fullstack-routing",
101102
"packages/playwright-tests/suspense-carousel",
102103
"packages/playwright-tests/nested-suspense",
103104
"packages/playwright-tests/cli-optimization",

packages/core/src/error_boundary.rs

+2-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::{
33
Properties, ScopeId, Template, TemplateAttribute, TemplateNode, VNode,
44
};
55
use std::{
6-
any::{Any, TypeId},
6+
any::Any,
77
backtrace::Backtrace,
88
cell::{Ref, RefCell},
99
error::Error,
@@ -493,11 +493,7 @@ impl Display for CapturedError {
493493
impl CapturedError {
494494
/// Downcast the error type into a concrete error type
495495
pub fn downcast<T: 'static>(&self) -> Option<&T> {
496-
if TypeId::of::<T>() == (*self.error).type_id() {
497-
self.error.as_any().downcast_ref::<T>()
498-
} else {
499-
None
500-
}
496+
self.error.as_any().downcast_ref::<T>()
501497
}
502498
}
503499

packages/fullstack/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ generational-box = { workspace = true }
2525
# Dioxus + SSR
2626
dioxus-ssr = { workspace = true, optional = true }
2727
dioxus-isrg = { workspace = true, optional = true }
28+
dioxus-router = { workspace = true, optional = true }
2829
hyper = { workspace = true, optional = true }
2930
http = { workspace = true, optional = true }
3031

@@ -96,6 +97,7 @@ server = [
9697
"dep:tokio-stream",
9798
"dep:dioxus-ssr",
9899
"dep:dioxus-isrg",
100+
"dep:dioxus-router",
99101
"dep:tower",
100102
"dep:hyper",
101103
"dep:http",

packages/fullstack/src/render.rs

+54-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use dioxus_cli_config::base_path;
66
use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
77
use dioxus_isrg::{CachedRender, IncrementalRendererError, RenderFreshness};
88
use dioxus_lib::document::Document;
9+
use dioxus_router::prelude::ParseRouteError;
910
use dioxus_ssr::Renderer;
1011
use futures_channel::mpsc::Sender;
1112
use futures_util::{Stream, StreamExt};
@@ -50,6 +51,14 @@ where
5051
}
5152
}
5253

54+
/// Errors that can occur during server side rendering before the initial chunk is sent down
55+
pub enum SSRError {
56+
/// An error from the incremental renderer. This should result in a 500 code
57+
Incremental(IncrementalRendererError),
58+
/// An error from the dioxus router. This should result in a 404 code
59+
Routing(ParseRouteError),
60+
}
61+
5362
struct SsrRendererPool {
5463
renderers: RwLock<Vec<Renderer>>,
5564
incremental_cache: Option<RwLock<dioxus_isrg::IncrementalRenderer>>,
@@ -112,7 +121,7 @@ impl SsrRendererPool {
112121
RenderFreshness,
113122
impl Stream<Item = Result<String, dioxus_isrg::IncrementalRendererError>>,
114123
),
115-
dioxus_isrg::IncrementalRendererError,
124+
SSRError,
116125
> {
117126
struct ReceiverWithDrop {
118127
receiver: futures_channel::mpsc::Receiver<
@@ -145,6 +154,8 @@ impl SsrRendererPool {
145154
Result<String, dioxus_isrg::IncrementalRendererError>,
146155
>(1000);
147156

157+
let (initial_result_tx, initial_result_rx) = futures_channel::oneshot::channel();
158+
148159
// before we even spawn anything, we can check synchronously if we have the route cached
149160
if let Some(freshness) = self.check_cached_route(&route, &mut into) {
150161
return Ok((
@@ -188,7 +199,7 @@ impl SsrRendererPool {
188199
virtual_dom.provide_root_context(Rc::new(history) as Rc<dyn dioxus_history::History>);
189200
virtual_dom.provide_root_context(document.clone() as std::rc::Rc<dyn Document>);
190201

191-
// poll the future, which may call server_context()
202+
// rebuild the virtual dom, which may call server_context()
192203
with_server_context(server_context.clone(), || virtual_dom.rebuild_in_place());
193204

194205
// If streaming is disabled, wait for the virtual dom to finish all suspense work
@@ -197,6 +208,41 @@ impl SsrRendererPool {
197208
ProvideServerContext::new(virtual_dom.wait_for_suspense(), server_context.clone())
198209
.await
199210
}
211+
// check if there are any errors
212+
let errors = with_server_context(server_context.clone(), || {
213+
virtual_dom.in_runtime(|| {
214+
let error_context: ErrorContext = ScopeId::APP
215+
.consume_context()
216+
.expect("The root should be under an error boundary");
217+
let errors = error_context.errors();
218+
errors.to_vec()
219+
})
220+
});
221+
if errors.is_empty() {
222+
// If routing was successful, we can return a 200 status and render into the stream
223+
_ = initial_result_tx.send(Ok(()));
224+
} else {
225+
// If there was an error while routing, return the error with a 400 status
226+
// Return a routing error if any of the errors were a routing error
227+
let routing_error = errors.iter().find_map(|err| err.downcast().cloned());
228+
if let Some(routing_error) = routing_error {
229+
_ = initial_result_tx.send(Err(SSRError::Routing(routing_error)));
230+
return;
231+
}
232+
#[derive(thiserror::Error, Debug)]
233+
#[error("{0}")]
234+
pub struct ErrorWhileRendering(String);
235+
let mut all_errors = String::new();
236+
for error in errors {
237+
all_errors += &error.to_string();
238+
all_errors += "\n"
239+
}
240+
let error = ErrorWhileRendering(all_errors);
241+
_ = initial_result_tx.send(Err(SSRError::Incremental(
242+
IncrementalRendererError::Other(Box::new(error)),
243+
)));
244+
return;
245+
}
200246

201247
let mut pre_body = String::new();
202248

@@ -325,6 +371,11 @@ impl SsrRendererPool {
325371
myself.renderers.write().unwrap().push(renderer);
326372
});
327373

374+
// Wait for the initial result which determines the status code
375+
initial_result_rx.await.map_err(|err| {
376+
SSRError::Incremental(IncrementalRendererError::Other(Box::new(err)))
377+
})??;
378+
328379
Ok((
329380
RenderFreshness::now(None),
330381
ReceiverWithDrop {
@@ -447,7 +498,7 @@ impl SSRState {
447498
RenderFreshness,
448499
impl Stream<Item = Result<String, dioxus_isrg::IncrementalRendererError>>,
449500
),
450-
dioxus_isrg::IncrementalRendererError,
501+
SSRError,
451502
> {
452503
self.renderers
453504
.clone()

packages/fullstack/src/server/mod.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ use http::header::*;
6969

7070
use std::sync::Arc;
7171

72+
use crate::render::SSRError;
7273
use crate::{prelude::*, ContextProviders};
7374

7475
/// A extension trait with utilities for integrating Dioxus with your Axum router.
@@ -413,10 +414,17 @@ pub async fn render_handler(
413414
apply_request_parts_to_response(headers, &mut response);
414415
Result::<http::Response<axum::body::Body>, StatusCode>::Ok(response)
415416
}
416-
Err(e) => {
417+
Err(SSRError::Incremental(e)) => {
417418
tracing::error!("Failed to render page: {}", e);
418419
Ok(report_err(e).into_response())
419420
}
421+
Err(SSRError::Routing(e)) => {
422+
tracing::trace!("Page not found: {}", e);
423+
Ok(Response::builder()
424+
.status(StatusCode::NOT_FOUND)
425+
.body(Body::from("Page not found"))
426+
.unwrap())
427+
}
420428
}
421429
}
422430

packages/isrg/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ pub enum IncrementalRendererError {
156156
/// An IO error occurred while rendering a route.
157157
#[error("IoError: {0}")]
158158
IoError(#[from] std::io::Error),
159-
/// An IO error occurred while rendering a route.
159+
/// An error occurred while rendering a route.
160160
#[error("Other: {0}")]
161161
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
162162
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// @ts-check
2+
const { test, expect } = require("@playwright/test");
3+
4+
// Wait for the build to finish
5+
async function waitForBuild(request) {
6+
for (let i = 0; i < 10; i++) {
7+
const build = await request.get("http://localhost:8888");
8+
let text = await build.text();
9+
if (!text.includes("Backend connection failed")) {
10+
return;
11+
}
12+
await new Promise((r) => setTimeout(r, 1000));
13+
}
14+
}
15+
16+
// The home and id routes should return 200
17+
test("home route", async ({ request }) => {
18+
await waitForBuild(request);
19+
const response = await request.get("http://localhost:8888");
20+
21+
expect(response.status()).toBe(200);
22+
23+
const text = await response.text();
24+
expect(text).toContain("Home");
25+
});
26+
27+
test("blog route", async ({ request }) => {
28+
await waitForBuild(request);
29+
const response = await request.get("http://localhost:8888/blog/123");
30+
31+
expect(response.status()).toBe(200);
32+
33+
const text = await response.text();
34+
expect(text).toContain("id: 123");
35+
});
36+
37+
// The error route should return 500
38+
test("error route", async ({ request }) => {
39+
await waitForBuild(request);
40+
const response = await request.get("http://localhost:8888/error");
41+
42+
expect(response.status()).toBe(500);
43+
});
44+
45+
// An unknown route should return 404
46+
test("unknown route", async ({ request }) => {
47+
await waitForBuild(request);
48+
const response = await request.get(
49+
"http://localhost:8888/this-route-does-not-exist"
50+
);
51+
52+
expect(response.status()).toBe(404);
53+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.dioxus
2+
dist
3+
target
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "dioxus-playwright-fullstack-routing-test"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
7+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8+
9+
[dependencies]
10+
dioxus = { workspace = true, features = ["fullstack", "router"] }
11+
serde = "1.0.159"
12+
tokio = { workspace = true, features = ["full"], optional = true }
13+
14+
[features]
15+
default = []
16+
server = ["dioxus/server", "dep:tokio"]
17+
web = ["dioxus/web"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// This test is used by playwright configured in the root of the repo
2+
// Tests:
3+
// - 200 Routes
4+
// - 404 Routes
5+
// - 500 Routes
6+
7+
#![allow(non_snake_case)]
8+
use dioxus::{prelude::*, CapturedError};
9+
10+
fn main() {
11+
dioxus::LaunchBuilder::new()
12+
.with_cfg(server_only! {
13+
dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
14+
})
15+
.launch(app);
16+
}
17+
18+
fn app() -> Element {
19+
rsx! { Router::<Route> {} }
20+
}
21+
22+
#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
23+
enum Route {
24+
#[route("/")]
25+
Home,
26+
27+
#[route("/blog/:id/")]
28+
Blog { id: i32 },
29+
30+
#[route("/error")]
31+
ThrowsError,
32+
}
33+
34+
#[component]
35+
fn Blog(id: i32) -> Element {
36+
rsx! {
37+
Link { to: Route::Home {}, "Go home" }
38+
"id: {id}"
39+
}
40+
}
41+
42+
#[component]
43+
fn ThrowsError() -> Element {
44+
return Err(RenderError::Aborted(CapturedError::from_display(
45+
"This route tests uncaught errors in the server",
46+
)));
47+
}
48+
49+
#[component]
50+
fn Home() -> Element {
51+
rsx! {
52+
"Home"
53+
}
54+
}

packages/playwright-tests/playwright.config.js

+9
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,15 @@ module.exports = defineConfig({
111111
reuseExistingServer: !process.env.CI,
112112
stdout: "pipe",
113113
},
114+
{
115+
cwd: path.join(process.cwd(), "fullstack-routing"),
116+
command:
117+
'cargo run --package dioxus-cli --release -- serve --force-sequential --platform web --addr "127.0.0.1" --port 8888',
118+
port: 8888,
119+
timeout: 50 * 60 * 1000,
120+
reuseExistingServer: !process.env.CI,
121+
stdout: "pipe",
122+
},
114123
{
115124
cwd: path.join(process.cwd(), "suspense-carousel"),
116125
command:

packages/router/src/components/router.rs

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use dioxus_lib::prelude::*;
22

3-
use std::str::FromStr;
4-
53
use crate::{
64
prelude::{provide_router_context, Outlet},
75
routable::Routable,
@@ -39,10 +37,7 @@ impl<R: Clone> PartialEq for RouterProps<R> {
3937
}
4038

4139
/// A component that renders the current route.
42-
pub fn Router<R: Routable + Clone>(props: RouterProps<R>) -> Element
43-
where
44-
<R as FromStr>::Err: std::fmt::Display,
45-
{
40+
pub fn Router<R: Routable + Clone>(props: RouterProps<R>) -> Element {
4641
use crate::prelude::{outlet::OutletContext, RouterContext};
4742

4843
use_hook(|| {

0 commit comments

Comments
 (0)