More fullstack fixes (#4737)

* allow `--server` only builds!

* rollback example

* capture `/:route` routes

* fix: properly implement file uploads in forms

* implement desktop form uploads

* Performant serialization of form bytes

* finish up file stuff

* cleanups

* add request headers to streaming context

* remove some unwraps
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-07 23:07:43 -07:00
1 parent fc01a002fd
commit 92792aded9
47 files changed
+1241 -703

No files matched your search

+81 -1
View File
@@ -1,4 +1,5 @@
use axum_core::response::IntoResponse;
use futures_util::TryStreamExt;
use http::StatusCode;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
@@ -91,6 +92,28 @@ impl ServerFnError {
code: None,
}
}
/// Create a new server error (status code 500) with a message and details.
pub async fn from_axum_response(resp: axum_core::response::Response) -> Self {
let status = resp.status();
let message = resp
.into_body()
.into_data_stream()
.try_fold(Vec::new(), |mut acc, chunk| async move {
acc.extend_from_slice(&chunk);
Ok(acc)
})
.await
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
.unwrap_or_else(|| status.canonical_reason().unwrap_or("").to_string());
ServerFnError::ServerError {
message,
code: Some(status.as_u16()),
details: None,
}
}
}
impl From<anyhow::Error> for ServerFnError {
@@ -159,7 +182,64 @@ impl From<HttpError> for ServerFnError {
impl IntoResponse for ServerFnError {
fn into_response(self) -> axum_core::response::Response {
todo!()
match self {
Self::ServerError {
message,
code,
details,
} => {
let status = code
.and_then(|c| StatusCode::from_u16(c).ok())
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let body = if let Some(details) = details {
serde_json::json!({
"error": message,
"details": details,
})
} else {
serde_json::json!({
"error": message,
})
};
let body = axum_core::body::Body::from(
serde_json::to_string(&body)
.unwrap_or_else(|_| "{\"error\":\"Internal Server Error\"}".to_string()),
);
axum_core::response::Response::builder()
.status(status)
.header("Content-Type", "application/json")
.body(body)
.unwrap_or_else(|_| {
axum_core::response::Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(axum_core::body::Body::from(
"{\"error\":\"Internal Server Error\"}",
))
.unwrap()
})
}
_ => {
let status: StatusCode = self.clone().into();
let body = axum_core::body::Body::from(
serde_json::json!({
"error": self.to_string(),
})
.to_string(),
);
axum_core::response::Response::builder()
.status(status)
.header("Content-Type", "application/json")
.body(body)
.unwrap_or_else(|_| {
axum_core::response::Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(axum_core::body::Body::from(
"{\"error\":\"Internal Server Error\"}",
))
.unwrap()
})
}
}
}
}
+48 -5
View File
@@ -1,5 +1,9 @@
use crate::ServerFnError;
use axum_core::{extract::FromRequest, response::IntoResponse};
use dioxus_core::try_consume_context;
use dioxus_signals::{ReadableExt, Signal, WritableExt};
use http::request::Parts;
use std::{cell::RefCell, rc::Rc};
/// The status of the streaming response
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -13,23 +17,26 @@ pub enum StreamingStatus {
}
/// The context dioxus fullstack provides for the status of streaming responses on the server
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Debug)]
pub struct StreamingContext {
current_status: Signal<StreamingStatus>,
request_headers: Rc<RefCell<http::request::Parts>>,
}
impl Default for StreamingContext {
fn default() -> Self {
Self::new()
impl PartialEq for StreamingContext {
fn eq(&self, other: &Self) -> bool {
self.current_status == other.current_status
&& Rc::ptr_eq(&self.request_headers, &other.request_headers)
}
}
impl StreamingContext {
/// Create a new streaming context. You should not need to call this directly. Dioxus fullstack will
/// provide this context for you.
pub fn new() -> Self {
pub fn new(parts: Parts) -> Self {
Self {
current_status: Signal::new(StreamingStatus::RenderingInitialChunk),
request_headers: Rc::new(RefCell::new(parts)),
}
}
@@ -48,6 +55,42 @@ impl StreamingContext {
pub fn current_status(&self) -> StreamingStatus {
*self.current_status.read()
}
/// Access the http request parts mutably. This will allow you to modify headers and other parts of the request.
pub fn parts_mut(&self) -> std::cell::RefMut<'_, http::request::Parts> {
self.request_headers.borrow_mut()
}
/// Extract an axum extractor from the current request. This will always use an empty body for the request,
/// since it's assumed that rendering the app is done under a `GET` request.
pub async fn extract<T: FromRequest<(), M>, M>() -> Result<T, ServerFnError> {
let this = Self::current()
.ok_or_else(|| ServerFnError::new("No StreamingContext found".to_string()))?;
let parts = this.request_headers.borrow_mut().clone();
let request =
axum_core::extract::Request::from_parts(parts, axum_core::body::Body::empty());
match T::from_request(request, &()).await {
Ok(res) => Ok(res),
Err(err) => {
let resp = err.into_response();
Err(ServerFnError::from_axum_response(resp).await)
}
}
}
/// Get the current `StreamingContext` if it exists. This will return `None` if called on the client
/// or outside of a streaming response on the server.
pub fn current() -> Option<Self> {
if let Some(rt) = dioxus_core::Runtime::try_current() {
let id = rt.try_current_scope_id()?;
if let Some(ctx) = rt.consume_context::<StreamingContext>(id) {
return Some(ctx);
}
}
None
}
}
/// Commit the initial chunk of the response. This will be called automatically if you are using the