Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ members = [
"packages/playwright-tests/web",
"packages/playwright-tests/fullstack",
"packages/playwright-tests/fullstack-mounted",
"packages/playwright-tests/fullstack-routing",
"packages/playwright-tests/suspense-carousel",
"packages/playwright-tests/nested-suspense",
"packages/playwright-tests/cli-optimization",
Expand Down
8 changes: 2 additions & 6 deletions packages/core/src/error_boundary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::{
Properties, ScopeId, Template, TemplateAttribute, TemplateNode, VNode,
};
use std::{
any::{Any, TypeId},
any::Any,
backtrace::Backtrace,
cell::{Ref, RefCell},
error::Error,
Expand Down Expand Up @@ -493,11 +493,7 @@ impl Display for CapturedError {
impl CapturedError {
/// Downcast the error type into a concrete error type
pub fn downcast<T: 'static>(&self) -> Option<&T> {
if TypeId::of::<T>() == (*self.error).type_id() {
self.error.as_any().downcast_ref::<T>()
} else {
None
}
self.error.as_any().downcast_ref::<T>()
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/fullstack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ generational-box = { workspace = true }
# Dioxus + SSR
dioxus-ssr = { workspace = true, optional = true }
dioxus-isrg = { workspace = true, optional = true }
dioxus-router = { workspace = true, optional = true }
hyper = { workspace = true, optional = true }
http = { workspace = true, optional = true }

Expand Down Expand Up @@ -96,6 +97,7 @@ server = [
"dep:tokio-stream",
"dep:dioxus-ssr",
"dep:dioxus-isrg",
"dep:dioxus-router",
"dep:tower",
"dep:hyper",
"dep:http",
Expand Down
57 changes: 54 additions & 3 deletions packages/fullstack/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use dioxus_cli_config::base_path;
use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
use dioxus_isrg::{CachedRender, IncrementalRendererError, RenderFreshness};
use dioxus_lib::document::Document;
use dioxus_router::prelude::ParseRouteError;
use dioxus_ssr::Renderer;
use futures_channel::mpsc::Sender;
use futures_util::{Stream, StreamExt};
Expand Down Expand Up @@ -50,6 +51,14 @@ where
}
}

/// Errors that can occur during server side rendering before the initial chunk is sent down
pub enum SSRError {
/// An error from the incremental renderer. This should result in a 500 code
Incremental(IncrementalRendererError),
/// An error from the dioxus router. This should result in a 404 code
Routing(ParseRouteError),
}

struct SsrRendererPool {
renderers: RwLock<Vec<Renderer>>,
incremental_cache: Option<RwLock<dioxus_isrg::IncrementalRenderer>>,
Expand Down Expand Up @@ -112,7 +121,7 @@ impl SsrRendererPool {
RenderFreshness,
impl Stream<Item = Result<String, dioxus_isrg::IncrementalRendererError>>,
),
dioxus_isrg::IncrementalRendererError,
SSRError,
> {
struct ReceiverWithDrop {
receiver: futures_channel::mpsc::Receiver<
Expand Down Expand Up @@ -145,6 +154,8 @@ impl SsrRendererPool {
Result<String, dioxus_isrg::IncrementalRendererError>,
>(1000);

let (initial_result_tx, initial_result_rx) = futures_channel::oneshot::channel();

// 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) {
return Ok((
Expand Down Expand Up @@ -188,7 +199,7 @@ impl SsrRendererPool {
virtual_dom.provide_root_context(Rc::new(history) as Rc<dyn dioxus_history::History>);
virtual_dom.provide_root_context(document.clone() as std::rc::Rc<dyn Document>);

// poll the future, which may call server_context()
// rebuild the virtual dom, which may call server_context()
with_server_context(server_context.clone(), || virtual_dom.rebuild_in_place());

// If streaming is disabled, wait for the virtual dom to finish all suspense work
Expand All @@ -197,6 +208,41 @@ impl SsrRendererPool {
ProvideServerContext::new(virtual_dom.wait_for_suspense(), server_context.clone())
.await
}
// check if there are any errors
let errors = with_server_context(server_context.clone(), || {
virtual_dom.in_runtime(|| {
let error_context: ErrorContext = ScopeId::APP
.consume_context()
.expect("The root should be under an error boundary");
let errors = error_context.errors();
errors.to_vec()
})
});
if errors.is_empty() {
// If routing was successful, we can return a 200 status and render into the stream
_ = initial_result_tx.send(Ok(()));
} else {
// If there was an error while routing, return the error with a 400 status
// Return a routing error if any of the errors were a routing error
let routing_error = errors.iter().find_map(|err| err.downcast().cloned());
if let Some(routing_error) = routing_error {
_ = initial_result_tx.send(Err(SSRError::Routing(routing_error)));
return;
}
#[derive(thiserror::Error, Debug)]
#[error("{0}")]
pub struct ErrorWhileRendering(String);
let mut all_errors = String::new();
for error in errors {
all_errors += &error.to_string();
all_errors += "\n"
}
let error = ErrorWhileRendering(all_errors);
_ = initial_result_tx.send(Err(SSRError::Incremental(
IncrementalRendererError::Other(Box::new(error)),
)));
return;
}

let mut pre_body = String::new();

Expand Down Expand Up @@ -325,6 +371,11 @@ impl SsrRendererPool {
myself.renderers.write().unwrap().push(renderer);
});

// Wait for the initial result which determines the status code
initial_result_rx.await.map_err(|err| {
SSRError::Incremental(IncrementalRendererError::Other(Box::new(err)))
})??;

Ok((
RenderFreshness::now(None),
ReceiverWithDrop {
Expand Down Expand Up @@ -447,7 +498,7 @@ impl SSRState {
RenderFreshness,
impl Stream<Item = Result<String, dioxus_isrg::IncrementalRendererError>>,
),
dioxus_isrg::IncrementalRendererError,
SSRError,
> {
self.renderers
.clone()
Expand Down
7 changes: 6 additions & 1 deletion packages/fullstack/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ use http::header::*;

use std::sync::Arc;

use crate::render::SSRError;
use crate::{prelude::*, ContextProviders};

/// A extension trait with utilities for integrating Dioxus with your Axum router.
Expand Down Expand Up @@ -413,10 +414,14 @@ pub async fn render_handler(
apply_request_parts_to_response(headers, &mut response);
Result::<http::Response<axum::body::Body>, StatusCode>::Ok(response)
}
Err(e) => {
Err(SSRError::Incremental(e)) => {
tracing::error!("Failed to render page: {}", e);
Ok(report_err(e).into_response())
}
Err(SSRError::Routing(e)) => Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(body::Body::new(format!("Page not found: {}", e)))
.unwrap()),
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/isrg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ pub enum IncrementalRendererError {
/// An IO error occurred while rendering a route.
#[error("IoError: {0}")]
IoError(#[from] std::io::Error),
/// An IO error occurred while rendering a route.
/// An error occurred while rendering a route.
#[error("Other: {0}")]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
53 changes: 53 additions & 0 deletions packages/playwright-tests/fullstack-routing.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// @ts-check
const { test, expect } = require("@playwright/test");

// Wait for the build to finish
async function waitForBuild(request) {
for (let i = 0; i < 10; i++) {
const build = await request.get("http://localhost:8888");
let text = await build.text();
if (!text.includes("Backend connection failed")) {
return;
}
await new Promise((r) => setTimeout(r, 1000));
}
}

// The home and id routes should return 200
test("home route", async ({ request }) => {
await waitForBuild(request);
const response = await request.get("http://localhost:8888");

expect(response.status()).toBe(200);

const text = await response.text();
expect(text).toContain("Home");
});

test("blog route", async ({ request }) => {
await waitForBuild(request);
const response = await request.get("http://localhost:8888/blog/123");

expect(response.status()).toBe(200);

const text = await response.text();
expect(text).toContain("id: 123");
});

// The error route should return 500
test("error route", async ({ request }) => {
await waitForBuild(request);
const response = await request.get("http://localhost:8888/error");

expect(response.status()).toBe(500);
});

// An unknown route should return 404
test("unknown route", async ({ request }) => {
await waitForBuild(request);
const response = await request.get(
"http://localhost:8888/this-route-does-not-exist"
);

expect(response.status()).toBe(404);
});
3 changes: 3 additions & 0 deletions packages/playwright-tests/fullstack-routing/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.dioxus
dist
target
17 changes: 17 additions & 0 deletions packages/playwright-tests/fullstack-routing/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "dioxus-playwright-fullstack-routing-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"] }
serde = "1.0.159"
tokio = { workspace = true, features = ["full"], optional = true }

[features]
default = []
server = ["dioxus/server", "dep:tokio"]
web = ["dioxus/web"]
54 changes: 54 additions & 0 deletions packages/playwright-tests/fullstack-routing/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// This test is used by playwright configured in the root of the repo
// Tests:
// - 200 Routes
// - 404 Routes
// - 500 Routes

#![allow(non_snake_case)]
use dioxus::{prelude::*, CapturedError};

fn main() {
dioxus::LaunchBuilder::new()
.with_cfg(server_only! {
dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
})
.launch(app);
}

fn app() -> Element {
rsx! { Router::<Route> {} }
}

#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
enum Route {
#[route("/")]
Home,

#[route("/blog/:id/")]
Blog { id: i32 },

#[route("/error")]
ThrowsError,
}

#[component]
fn Blog(id: i32) -> Element {
rsx! {
Link { to: Route::Home {}, "Go home" }
"id: {id}"
}
}

#[component]
fn ThrowsError() -> Element {
return Err(RenderError::Aborted(CapturedError::from_display(
"This route tests uncaught errors in the server",
)));
}

#[component]
fn Home() -> Element {
rsx! {
"Home"
}
}
9 changes: 9 additions & 0 deletions packages/playwright-tests/playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ module.exports = defineConfig({
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "fullstack-routing"),
command:
'cargo run --package dioxus-cli --release -- serve --force-sequential --platform web --addr "127.0.0.1" --port 8888',
port: 8888,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "suspense-carousel"),
command:
Expand Down
5 changes: 4 additions & 1 deletion packages/router/src/components/outlet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ use dioxus_lib::prelude::*;
/// # vdom.rebuild_in_place();
/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1><p>Child</p>");
/// ```
pub fn Outlet<R: Routable + Clone>() -> Element {
pub fn Outlet<R: Routable + Clone>() -> Element
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
OutletContext::<R>::render()
}
1 change: 1 addition & 0 deletions packages/router/src/contexts/outlet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ impl<R> OutletContext<R> {
pub(crate) fn render() -> Element
where
R: Routable + Clone,
<R as std::str::FromStr>::Err: std::fmt::Display,
{
let router = use_router_internal().expect("Outlet must be inside of a router");
let outlet: OutletContext<R> = use_outlet_context();
Expand Down
Loading
Loading