Return a 404 when paring the route fails (#3860)
* return a 404 when paring the route fails * add a playwright test for fullstack routing status codes * Fix new playwright test * document SSRError * Make SSRError public and fix clippy * Move the error display bound to the routable trait * fix clippy * return a more generic 404 message * fix router tests
This commit is contained in:
1 parent
7b7357037f
commit
09a4b5eb4e
18 files changed
+253
-46
No files matched your search
@@ -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 }
|
||||
|
||||
@@ -96,6 +97,7 @@ server = [
|
||||
"dep:tokio-stream",
|
||||
"dep:dioxus-ssr",
|
||||
"dep:dioxus-isrg",
|
||||
"dep:dioxus-router",
|
||||
"dep:tower",
|
||||
"dep:hyper",
|
||||
"dep:http",
|
||||
|
||||
@@ -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};
|
||||
@@ -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>>,
|
||||
@@ -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<
|
||||
@@ -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((
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
@@ -447,7 +498,7 @@ impl SSRState {
|
||||
RenderFreshness,
|
||||
impl Stream<Item = Result<String, dioxus_isrg::IncrementalRendererError>>,
|
||||
),
|
||||
dioxus_isrg::IncrementalRendererError,
|
||||
SSRError,
|
||||
> {
|
||||
self.renderers
|
||||
.clone()
|
||||
|
||||
@@ -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.
|
||||
@@ -413,10 +414,17 @@ 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)) => {
|
||||
tracing::trace!("Page not found: {}", e);
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::from("Page not found"))
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user