Skip to content

Commit 25abfa3

Browse files
committed
encode redirect as server error
1 parent c6ec025 commit 25abfa3

File tree

7 files changed

+61
-123
lines changed

7 files changed

+61
-123
lines changed

packages/core/src/control_flow.rs

Lines changed: 0 additions & 19 deletions
This file was deleted.

packages/core/src/lib.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ mod effect;
1010
mod error_boundary;
1111
mod events;
1212
mod fragment;
13-
mod control_flow;
1413
mod generational_box;
1514
mod global_context;
1615
mod launch;
@@ -31,11 +30,6 @@ mod virtual_dom;
3130

3231
mod hotreload_utils;
3332

34-
// This is intentionally public (but hidden from docs) so integration crates can use it without
35-
// creating dependency cycles back into core.
36-
#[doc(hidden)]
37-
pub use control_flow::RedirectControlFlow;
38-
3933
/// Items exported from this module are used in macros and should not be used directly.
4034
#[doc(hidden)]
4135
pub mod internal {

packages/core/src/scope_arena.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -108,20 +108,7 @@ impl VirtualDom {
108108
fn handle_element_return(&self, node: &mut Element, scope: &Scope) {
109109
match node {
110110
Err(RenderError::Error(e)) => {
111-
// Redirects are expected control-flow in some integrations (like fullstack SSR)
112-
// and shouldn't be logged as errors.
113-
//
114-
// We detect these via a core-owned marker error (`RedirectControlFlow`) attached
115-
// as a `source()` by integration crates to avoid dependency cycles.
116-
let is_redirect = e
117-
.chain()
118-
.any(|cause| cause.is::<crate::RedirectControlFlow>());
119-
120-
if is_redirect {
121-
tracing::info!("Redirect while rendering component `{}`: {e}", scope.name);
122-
} else {
123-
tracing::error!("Error while rendering component `{}`: {e}", scope.name);
124-
}
111+
tracing::error!("Error while rendering component `{}`: {e}", scope.name);
125112
self.runtime.throw_error(scope.id, e.clone());
126113
}
127114
Err(RenderError::Suspended(e)) => {

packages/fullstack-core/src/error.rs

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use axum_core::response::IntoResponse;
22
use futures_util::TryStreamExt;
3+
use http::header::LOCATION;
34
use http::StatusCode;
45
use serde::{Deserialize, Serialize};
5-
use std::fmt::Debug;
66

77
use crate::HttpError;
88

@@ -30,7 +30,7 @@ pub enum ServerFnError {
3030
/// The `details` field can optionally contain additional structured information about the error.
3131
/// When passing typed errors from the server to the client, the `details` field contains the serialized
3232
/// representation of the error.
33-
#[error("error running server function: {message} (details: {details:#?})")]
33+
#[error("error running server function: {message} (details: {details:?})")]
3434
ServerError {
3535
/// A human-readable message describing the error.
3636
message: String,
@@ -81,23 +81,19 @@ pub enum ServerFnError {
8181
/// Occurs on the server if there is an error creating an HTTP response.
8282
#[error("error creating response {0}")]
8383
Response(String),
84+
}
8485

85-
/// A redirect response returned while running a server function or extractor.
86-
///
87-
/// This is treated as control-flow rather than an ordinary error so we can preserve the
88-
/// `Location` header (which is otherwise lost when converting axum responses into `ServerFnError`).
89-
#[error("redirect ({code}) to {location}")]
90-
Redirect {
91-
/// HTTP status code associated with the redirect (typically 302/303/307/308).
92-
code: u16,
93-
/// The value of the `Location` header.
94-
location: String,
95-
/// Marker source to let core treat this as expected control-flow (not an error log).
96-
#[doc(hidden)]
97-
#[serde(skip, default)]
98-
#[source]
99-
control_flow: dioxus_core::RedirectControlFlow,
100-
},
86+
// Reserved key used to encode redirects in `ServerFnError::ServerError.details`.
87+
const REDIRECT_DETAILS_KEY: &str = "__dioxus_redirect";
88+
89+
fn is_redirection_code(code: u16) -> bool {
90+
(300..400).contains(&code)
91+
}
92+
93+
fn redirect_location_from_details(details: &Option<serde_json::Value>) -> Option<&str> {
94+
let details = details.as_ref()?;
95+
let obj = details.as_object()?;
96+
obj.get(REDIRECT_DETAILS_KEY)?.as_str()
10197
}
10298

10399
impl ServerFnError {
@@ -112,18 +108,38 @@ impl ServerFnError {
112108

113109
/// Create a redirect error (control-flow) with a status code and `Location`.
114110
pub fn redirect(code: u16, location: impl Into<String>) -> Self {
115-
ServerFnError::Redirect {
111+
let location = location.into();
112+
ServerFnError::ServerError {
113+
message: format!("redirect ({code}) to {location}"),
114+
details: Some(serde_json::json!({ REDIRECT_DETAILS_KEY: location })),
116115
code,
117-
location: location.into(),
118-
control_flow: dioxus_core::RedirectControlFlow::default(),
116+
}
117+
}
118+
119+
/// Returns the redirect location if this error represents a redirect.
120+
pub fn redirect_location(&self) -> Option<&str> {
121+
match self {
122+
ServerFnError::ServerError { code, details, .. } if is_redirection_code(*code) => {
123+
redirect_location_from_details(details)
124+
}
125+
_ => None,
119126
}
120127
}
121128

122129
/// Create a new server error (status code 500) with a message and details.
123130
pub async fn from_axum_response(resp: axum_core::response::Response) -> Self {
124-
let status = resp.status();
125-
let message = resp
126-
.into_body()
131+
let (parts, body) = resp.into_parts();
132+
let status = parts.status;
133+
134+
// If this is a redirect, preserve the location in structured details so other layers
135+
// (like SSR) can turn it back into a redirect response.
136+
if status.is_redirection() {
137+
if let Some(location) = parts.headers.get(LOCATION).and_then(|v| v.to_str().ok()) {
138+
return ServerFnError::redirect(status.as_u16(), location);
139+
}
140+
}
141+
142+
let message = body
127143
.into_data_stream()
128144
.try_fold(Vec::new(), |mut acc, chunk| async move {
129145
acc.extend_from_slice(&chunk);
@@ -164,9 +180,6 @@ impl From<ServerFnError> for http::StatusCode {
164180
ServerFnError::ServerError { code, .. } => {
165181
http::StatusCode::from_u16(code).unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR)
166182
}
167-
ServerFnError::Redirect { code, .. } => {
168-
http::StatusCode::from_u16(code).unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR)
169-
}
170183
ServerFnError::Request(err) => match err {
171184
RequestError::Status(_, code) => http::StatusCode::from_u16(code)
172185
.unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR),
@@ -195,7 +208,6 @@ impl From<ServerFnError> for HttpError {
195208
fn from(value: ServerFnError) -> Self {
196209
let status = StatusCode::from_u16(match &value {
197210
ServerFnError::ServerError { code, .. } => *code,
198-
ServerFnError::Redirect { code, .. } => *code,
199211
_ => 500,
200212
})
201213
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
@@ -225,23 +237,6 @@ impl From<HttpError> for ServerFnError {
225237
impl IntoResponse for ServerFnError {
226238
fn into_response(self) -> axum_core::response::Response {
227239
match self {
228-
Self::Redirect { code, location, .. } => {
229-
use http::header::LOCATION;
230-
let status =
231-
StatusCode::from_u16(code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
232-
axum_core::response::Response::builder()
233-
.status(status)
234-
.header(LOCATION, location)
235-
.body(axum_core::body::Body::empty())
236-
.unwrap_or_else(|_| {
237-
axum_core::response::Response::builder()
238-
.status(StatusCode::INTERNAL_SERVER_ERROR)
239-
.body(axum_core::body::Body::from(
240-
"{\"error\":\"Internal Server Error\"}",
241-
))
242-
.unwrap()
243-
})
244-
}
245240
Self::ServerError {
246241
message,
247242
code,

packages/fullstack-core/src/streaming.rs

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use crate::{HttpError, ServerFnError};
22
use axum_core::extract::FromRequest;
33
use axum_core::response::IntoResponse;
44
use dioxus_core::{CapturedError, ReactiveContext};
5-
use http::header::LOCATION;
65
use http::StatusCode;
76
use http::{request::Parts, HeaderMap};
87
use parking_lot::RwLock;
@@ -147,19 +146,6 @@ impl FullstackContext {
147146
Ok(res) => Ok(res),
148147
Err(err) => {
149148
let resp = err.into_response();
150-
151-
// Preserve redirects from axum-style extractors (3xx + Location) as control-flow.
152-
// If we collapse this response into `ServerFnError::ServerError`, we lose headers like
153-
// `Location` and redirects silently stop working.
154-
let status = resp.status();
155-
if status.is_redirection() {
156-
if let Some(location) = resp.headers().get(LOCATION) {
157-
if let Ok(location) = location.to_str() {
158-
return Err(ServerFnError::redirect(status.as_u16(), location));
159-
}
160-
}
161-
}
162-
163149
Err(ServerFnError::from_axum_response(resp).await)
164150
}
165151
}
@@ -397,12 +383,12 @@ mod tests {
397383
.await
398384
.unwrap_err();
399385

400-
match err {
401-
ServerFnError::Redirect { code, location, .. } => {
402-
assert_eq!(code, StatusCode::TEMPORARY_REDIRECT.as_u16());
403-
assert_eq!(location, "/sign-in");
386+
match &err {
387+
ServerFnError::ServerError { code, .. } => {
388+
assert_eq!(*code, StatusCode::TEMPORARY_REDIRECT.as_u16());
389+
assert_eq!(err.redirect_location(), Some("/sign-in"));
404390
}
405-
other => panic!("expected ServerFnError::Redirect, got {other:?}"),
391+
other => panic!("expected redirect-like ServerFnError, got {other:?}"),
406392
}
407393
});
408394
}

packages/fullstack-server/src/ssr.rs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -262,28 +262,28 @@ impl SsrRendererPool {
262262
// todo - the user is allowed to return anything that impls `From<ServerFnError>`
263263
// we need to eventually be able to downcast that and get the status code from it
264264
if let Some(server_fn_error) = error.downcast_ref::<ServerFnError>() {
265-
match server_fn_error {
266-
ServerFnError::ServerError { message, code, .. } => {
265+
// Redirects are encoded as `ServerError` with a redirect status and structured details.
266+
if let Some(location) = server_fn_error.redirect_location() {
267+
if let ServerFnError::ServerError { code, .. } = server_fn_error {
267268
status_code = Some(
268269
(*code)
269270
.try_into()
270271
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
271272
);
272-
out_message = Some(message.clone());
273273
}
274-
ServerFnError::Redirect { code, location, .. } => {
275-
status_code = Some(
276-
(*code)
277-
.try_into()
278-
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
279-
);
280-
// Redirects are control-flow; no message body by default.
281-
out_message = None;
282-
if let Ok(value) = HeaderValue::from_str(location) {
283-
out_headers.insert(LOCATION, value);
284-
}
274+
// Redirects are control-flow; no message body by default.
275+
out_message = None;
276+
if let Ok(value) = HeaderValue::from_str(location) {
277+
out_headers.insert(LOCATION, value);
285278
}
286-
_ => {}
279+
} else if let ServerFnError::ServerError { message, code, .. } = server_fn_error
280+
{
281+
status_code = Some(
282+
(*code)
283+
.try_into()
284+
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
285+
);
286+
out_message = Some(message.clone());
287287
}
288288
}
289289

packages/fullstack/src/magic.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -428,11 +428,6 @@ mod decode_ok {
428428
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))
429429
}
430430

431-
ServerFnError::Redirect { code, .. } => {
432-
Err(StatusCode::from_u16(code)
433-
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))
434-
}
435-
436431
ServerFnError::Registration(_) | ServerFnError::MiddlewareError(_) => {
437432
Err(StatusCode::INTERNAL_SERVER_ERROR)
438433
}

0 commit comments

Comments
 (0)