From 9ac211f61d7d49738096311608b8606a45e0f572 Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:53:45 -0700 Subject: [PATCH 01/18] fix: Set content type header for server function errors Problem ------- When a server function return an error, the content type header is not set. Solution -------- Use the `FromServerFnError::Encoder::CONTENT_TYPE` associated constant to set the content type header on the returned API response. Note: A similar issue occurs if an error is [returned in Leptos's middleware chain](https://github.com/leptos-rs/leptos/blob/b986fe11dc63a803d026f9ccae804e880c2a702b/server_fn/src/middleware/mod.rs#L77). However, I believe resolving that will require adding a field to the `BoxedService`, which would be an API-breaking change. --- server_fn/src/lib.rs | 10 ++++++---- server_fn/src/response/actix.rs | 7 +++++++ server_fn/src/response/generic.rs | 7 +++++++ server_fn/src/response/http.rs | 7 +++++++ server_fn/src/response/mod.rs | 7 ++++++- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 612b254c86..da47ffcfd0 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -307,16 +307,18 @@ pub trait ServerFn: Send + Sized { .await .map(|res| (res, None)) .unwrap_or_else(|e| { - ( + let mut response = <::Server as crate::Server< Self::Error, Self::InputStreamError, Self::OutputStreamError, >>::Response::error_response( Self::PATH, e.ser() - ), - Some(e), - ) + ); + let content_type = + ::Encoder::CONTENT_TYPE; + response.content_type(content_type); + (response, Some(e)) }); // if it accepts HTML, we'll redirect to the Referer diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index c823c6ac9e..454621b65b 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -12,6 +12,7 @@ use actix_web::{ }; use bytes::Bytes; use futures::{Stream, StreamExt}; +use http::header::CONTENT_TYPE; use send_wrapper::SendWrapper; /// A wrapped Actix response. @@ -80,6 +81,12 @@ impl Res for ActixResponse { )) } + fn content_type(&mut self, content_type: &str) { + if let Ok(content_type) = HeaderValue::from_str(content_type) { + self.0.headers_mut().insert(CONTENT_TYPE, content_type); + } + } + fn redirect(&mut self, path: &str) { if let Ok(path) = HeaderValue::from_str(path) { *self.0.status_mut() = StatusCode::FOUND; diff --git a/server_fn/src/response/generic.rs b/server_fn/src/response/generic.rs index 6952dd1e81..3ed01e62a2 100644 --- a/server_fn/src/response/generic.rs +++ b/server_fn/src/response/generic.rs @@ -100,6 +100,13 @@ impl Res for Response { .unwrap() } + fn content_type(&mut self, content_type: &str) { + if let Ok(content_type) = HeaderValue::from_str(content_type) { + self.headers_mut() + .insert(header::CONTENT_TYPE, content_type); + } + } + fn redirect(&mut self, path: &str) { if let Ok(path) = HeaderValue::from_str(path) { self.headers_mut().insert(header::LOCATION, path); diff --git a/server_fn/src/response/http.rs b/server_fn/src/response/http.rs index 65e1b41832..e6f2842643 100644 --- a/server_fn/src/response/http.rs +++ b/server_fn/src/response/http.rs @@ -60,6 +60,13 @@ impl Res for Response { .unwrap() } + fn content_type(&mut self, content_type: &str) { + if let Ok(content_type) = HeaderValue::from_str(content_type) { + self.headers_mut() + .insert(header::CONTENT_TYPE, content_type); + } + } + fn redirect(&mut self, path: &str) { if let Ok(path) = HeaderValue::from_str(path) { self.headers_mut().insert(header::LOCATION, path); diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 224713c149..32384f41b9 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -39,7 +39,8 @@ where pub trait Res { /// Converts an error into a response, with a `500` status code and the error text as its body. fn error_response(path: &str, err: Bytes) -> Self; - + /// Set the content type header for the response. + fn content_type(&mut self, #[allow(unused_variables)] content_type: &str) {} /// Redirect the response by setting a 302 code and Location header. fn redirect(&mut self, path: &str); } @@ -103,6 +104,10 @@ impl Res for BrowserMockRes { unreachable!() } + fn content_type(&mut self, _content_type: &str) { + unreachable!() + } + fn redirect(&mut self, _path: &str) { unreachable!() } From ed942bfcf5684e88b6ab6eb8e4642eed9d2efb6b Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Wed, 6 Aug 2025 16:14:07 -0700 Subject: [PATCH 02/18] Use CONTENT_TYPE header from `actix` re-export --- server_fn/src/response/actix.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index 454621b65b..749b290417 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -5,14 +5,13 @@ use crate::error::{ use actix_web::{ http::{ header, - header::{HeaderValue, LOCATION}, + header::{HeaderValue, CONTENT_TYPE, LOCATION}, StatusCode, }, HttpResponse, }; use bytes::Bytes; use futures::{Stream, StreamExt}; -use http::header::CONTENT_TYPE; use send_wrapper::SendWrapper; /// A wrapped Actix response. From bd317c8872923b52a0821a8e0e950f70194d2968 Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Wed, 6 Aug 2025 16:15:44 -0700 Subject: [PATCH 03/18] Add `-` in doc comment --- server_fn/src/response/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 32384f41b9..3d303b7a56 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -39,7 +39,7 @@ where pub trait Res { /// Converts an error into a response, with a `500` status code and the error text as its body. fn error_response(path: &str, err: Bytes) -> Self; - /// Set the content type header for the response. + /// Set the content-type header for the response. fn content_type(&mut self, #[allow(unused_variables)] content_type: &str) {} /// Redirect the response by setting a 302 code and Location header. fn redirect(&mut self, path: &str); From a5078726f2cd9cf7e6f01fe65724d504caf39657 Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Thu, 7 Aug 2025 16:55:48 -0700 Subject: [PATCH 04/18] Per PR suggestion, fix with an API-breaking change --- Cargo.lock | 73 +++++++++++++++++++++++++++++++ Cargo.toml | 1 + server_fn/Cargo.toml | 1 + server_fn/src/client.rs | 3 +- server_fn/src/codec/stream.rs | 4 +- server_fn/src/error.rs | 25 ++++++++--- server_fn/src/lib.rs | 26 ++++++----- server_fn/src/middleware/mod.rs | 39 ++++++++++------- server_fn/src/request/axum.rs | 7 +-- server_fn/src/response/actix.rs | 14 +++--- server_fn/src/response/browser.rs | 3 +- server_fn/src/response/generic.rs | 16 +++---- server_fn/src/response/http.rs | 22 ++++------ server_fn/src/response/mod.rs | 11 ++--- 14 files changed, 162 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ec15a039a..3f30398ec6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -498,6 +498,31 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "3.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d9ef19ae5263a138da9a86871eca537478ab0332a7770bac7e3f08b801f89f" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577ae008f2ca11ca7641bd44601002ee5ab49ef0af64846ce1ab6057218a5cc1" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.104", +] + [[package]] name = "brotli" version = "8.0.1" @@ -828,6 +853,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b136475da5ef7b6ac596c0e956e37bad51b85b987ff3d5e230e964936736b2" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b44ad32f92b75fb438b04b68547e521a548be8acc339a6dacc4a7121488f53e6" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.104", +] + +[[package]] +name = "darling_macro" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5be8a7a562d315a5b92a630c30cec6bcf663e6673f00fbb69cca66a6f521b9" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.104", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -1655,6 +1715,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -3288,6 +3354,7 @@ dependencies = [ "actix-ws", "axum", "base64", + "bon", "bytes", "ciborium", "const-str", @@ -3488,6 +3555,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index 606e2b03bf..27e4a7832d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,6 +170,7 @@ async-lock = { default-features = false, version = "3.4.0" } base16 = { default-features = false, version = "0.2.1" } digest = { default-features = false, version = "0.10.7" } sha2 = { default-features = false, version = "0.10.8" } +bon = { default-features = false, version = "3.6.5" } [profile.release] codegen-units = 1 diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 6b1e4f76c8..36988d0b1e 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -25,6 +25,7 @@ send_wrapper = { features = [ "futures", ], optional = true, workspace = true, default-features = true } thiserror = { workspace = true, default-features = true } +bon = { workspace = true } # registration system inventory = { optional = true, workspace = true, default-features = true } diff --git a/server_fn/src/client.rs b/server_fn/src/client.rs index dbf974950b..bfc9bb9733 100644 --- a/server_fn/src/client.rs +++ b/server_fn/src/client.rs @@ -141,7 +141,8 @@ pub mod browser { Err(OutputStreamError::from_server_fn_error( ServerFnErrorErr::Request(err.to_string()), ) - .ser()) + .ser() + .body) } }); let stream = SendWrapper::new(stream); diff --git a/server_fn/src/codec/stream.rs b/server_fn/src/codec/stream.rs index a6c4183242..9dadb2c644 100644 --- a/server_fn/src/codec/stream.rs +++ b/server_fn/src/codec/stream.rs @@ -120,7 +120,7 @@ where async fn into_res(self) -> Result { Response::try_from_stream( Streaming::CONTENT_TYPE, - self.into_inner().map_err(|e| e.ser()), + self.into_inner().map_err(|e| e.ser().body), ) } } @@ -255,7 +255,7 @@ where Response::try_from_stream( Streaming::CONTENT_TYPE, self.into_inner() - .map(|stream| stream.map(Into::into).map_err(|e| e.ser())), + .map(|stream| stream.map(Into::into).map_err(|e| e.ser().body)), ) } } diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index c8944d7b8b..ca1b76ffc2 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -474,7 +474,7 @@ impl ServerFnUrlError { let mut url = Url::parse(base)?; url.query_pairs_mut() .append_pair("__path", &self.path) - .append_pair("__err", &URL_SAFE.encode(self.error.ser())); + .append_pair("__err", &URL_SAFE.encode(self.error.ser().body)); Ok(url) } @@ -536,7 +536,7 @@ impl Display for ServerFnErrorWrapper { write!( f, "{}", - ::into_encoded_string(self.0.ser()) + ::into_encoded_string(self.0.ser().body) ) } } @@ -560,6 +560,17 @@ impl FromStr for ServerFnErrorWrapper { } } +/// Response parts returned by [`FromServerFnError::ser`] to be returned to the client. +#[derive(bon::Builder)] +#[non_exhaustive] +pub struct ServerFnErrorResponseParts { + /// The raw [`Bytes`] of the serialized error. + pub body: Bytes, + /// The value of the `CONTENT_TYPE` associated constant for the `FromServerFnError` + /// implementation. Used to set the `content-type` header in http responses. + pub content_type: &'static str, +} + /// A trait for types that can be returned from a server function. pub trait FromServerFnError: std::fmt::Debug + Sized + 'static { /// The encoding strategy used to serialize and deserialize this error type. Must implement the [`Encodes`](server_fn::Encodes) trait for references to the error type. @@ -569,8 +580,8 @@ pub trait FromServerFnError: std::fmt::Debug + Sized + 'static { fn from_server_fn_error(value: ServerFnErrorErr) -> Self; /// Converts the custom error type to a [`String`]. - fn ser(&self) -> Bytes { - Self::Encoder::encode(self).unwrap_or_else(|e| { + fn ser(&self) -> ServerFnErrorResponseParts { + let body = Self::Encoder::encode(self).unwrap_or_else(|e| { Self::Encoder::encode(&Self::from_server_fn_error( ServerFnErrorErr::Serialization(e.to_string()), )) @@ -578,7 +589,11 @@ pub trait FromServerFnError: std::fmt::Debug + Sized + 'static { "error serializing should success at least with the \ Serialization error", ) - }) + }); + ServerFnErrorResponseParts::builder() + .body(body) + .content_type(Self::Encoder::CONTENT_TYPE) + .build() } /// Deserializes the custom error type from a [`&str`]. diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index da47ffcfd0..211abcd570 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -307,18 +307,16 @@ pub trait ServerFn: Send + Sized { .await .map(|res| (res, None)) .unwrap_or_else(|e| { - let mut response = + ( <::Server as crate::Server< Self::Error, Self::InputStreamError, Self::OutputStreamError, >>::Response::error_response( Self::PATH, e.ser() - ); - let content_type = - ::Encoder::CONTENT_TYPE; - response.content_type(content_type); - (response, Some(e)) + ), + Some(e), + ) }); // if it accepts HTML, we'll redirect to the Referer @@ -669,8 +667,9 @@ where ServerFnErrorErr::Serialization(e.to_string()), ) .ser() + .body }), - Err(err) => Err(err.ser()), + Err(err) => Err(err.ser().body), }; serialize_result(result) }); @@ -713,9 +712,10 @@ where ), ) .ser() + .body }) } - Err(err) => Err(err.ser()), + Err(err) => Err(err.ser().body), }; let result = serialize_result(result); if sink.send(result).await.is_err() { @@ -783,7 +783,8 @@ fn deserialize_result( return Err(E::from_server_fn_error( ServerFnErrorErr::Deserialization("Data is empty".into()), ) - .ser()); + .ser() + .body); } let tag = bytes[0]; @@ -795,7 +796,8 @@ fn deserialize_result( _ => Err(E::from_server_fn_error(ServerFnErrorErr::Deserialization( "Invalid data tag".into(), )) - .ser()), // Invalid tag + .ser() + .body), // Invalid tag } } @@ -885,7 +887,7 @@ pub struct ServerFnTraitObj { method: Method, handler: fn(Req) -> Pin + Send>>, middleware: fn() -> MiddlewareSet, - ser: fn(ServerFnErrorErr) -> Bytes, + ser: middleware::ServerFnErrorSerializer, } impl ServerFnTraitObj { @@ -961,7 +963,7 @@ where fn run( &mut self, req: Req, - _ser: fn(ServerFnErrorErr) -> Bytes, + _err_ser: middleware::ServerFnErrorSerializer, ) -> Pin + Send>> { let handler = self.handler; Box::pin(async move { handler(req).await }) diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 508ab76340..76e357a0ee 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -1,5 +1,4 @@ -use crate::error::ServerFnErrorErr; -use bytes::Bytes; +use crate::error::{ServerFnErrorErr, ServerFnErrorResponseParts}; use std::{future::Future, pin::Pin}; /// An abstraction over a middleware layer, which can be used to add additional @@ -12,7 +11,7 @@ pub trait Layer: Send + Sync + 'static { /// A type-erased service, which takes an HTTP request and returns a response. pub struct BoxedService { /// A function that converts a [`ServerFnErrorErr`] into a string. - pub ser: fn(ServerFnErrorErr) -> Bytes, + pub ser: ServerFnErrorSerializer, /// The inner service. pub service: Box + Send>, } @@ -20,7 +19,7 @@ pub struct BoxedService { impl BoxedService { /// Constructs a type-erased service from this service. pub fn new( - ser: fn(ServerFnErrorErr) -> Bytes, + ser: ServerFnErrorSerializer, service: impl Service + Send + 'static, ) -> Self { Self { @@ -38,22 +37,25 @@ impl BoxedService { } } +/// todo +pub type ServerFnErrorSerializer = + fn(ServerFnErrorErr) -> ServerFnErrorResponseParts; + /// A service converts an HTTP request into a response. pub trait Service { /// Converts a request into a response. fn run( &mut self, req: Request, - ser: fn(ServerFnErrorErr) -> Bytes, + err_ser: ServerFnErrorSerializer, ) -> Pin + Send>>; } #[cfg(feature = "axum-no-default")] mod axum { - use super::{BoxedService, Service}; + use super::{BoxedService, ServerFnErrorSerializer, Service}; use crate::{error::ServerFnErrorErr, response::Res, ServerFnError}; use axum::body::Body; - use bytes::Bytes; use http::{Request, Response}; use std::{future::Future, pin::Pin}; @@ -66,14 +68,15 @@ mod axum { fn run( &mut self, req: Request, - ser: fn(ServerFnErrorErr) -> Bytes, + err_ser: ServerFnErrorSerializer, ) -> Pin> + Send>> { let path = req.uri().path().to_string(); let inner = self.call(req); Box::pin(async move { inner.await.unwrap_or_else(|e| { - let err = - ser(ServerFnErrorErr::MiddlewareError(e.to_string())); + let err = err_ser(ServerFnErrorErr::MiddlewareError( + e.to_string(), + )); Response::::error_response(&path, err) }) }) @@ -125,13 +128,13 @@ mod axum { #[cfg(feature = "actix-no-default")] mod actix { + use crate::middleware::ServerFnErrorSerializer; use crate::{ error::ServerFnErrorErr, request::actix::ActixRequest, response::{actix::ActixResponse, Res}, }; use actix_web::{HttpRequest, HttpResponse}; - use bytes::Bytes; use std::{future::Future, pin::Pin}; impl super::Service for S @@ -143,14 +146,15 @@ mod actix { fn run( &mut self, req: HttpRequest, - ser: fn(ServerFnErrorErr) -> Bytes, + err_ser: ServerFnErrorSerializer, ) -> Pin + Send>> { let path = req.uri().path().to_string(); let inner = self.call(req); Box::pin(async move { inner.await.unwrap_or_else(|e| { - let err = - ser(ServerFnErrorErr::MiddlewareError(e.to_string())); + let err = err_ser(ServerFnErrorErr::MiddlewareError( + e.to_string(), + )); ActixResponse::error_response(&path, err).take() }) }) @@ -166,14 +170,15 @@ mod actix { fn run( &mut self, req: ActixRequest, - ser: fn(ServerFnErrorErr) -> Bytes, + err_ser: ServerFnErrorSerializer, ) -> Pin + Send>> { let path = req.0 .0.uri().path().to_string(); let inner = self.call(req.0.take().0); Box::pin(async move { ActixResponse::from(inner.await.unwrap_or_else(|e| { - let err = - ser(ServerFnErrorErr::MiddlewareError(e.to_string())); + let err = err_ser(ServerFnErrorErr::MiddlewareError( + e.to_string(), + )); ActixResponse::error_response(&path, err).take() })) }) diff --git a/server_fn/src/request/axum.rs b/server_fn/src/request/axum.rs index 1e6471ff6f..4f41a7815d 100644 --- a/server_fn/src/request/axum.rs +++ b/server_fn/src/request/axum.rs @@ -70,6 +70,7 @@ where e.to_string(), )) .ser() + .body }) })) } @@ -124,7 +125,7 @@ where .on_failed_upgrade({ let mut outgoing_tx = outgoing_tx.clone(); move |err: axum::Error| { - _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(err.to_string())).ser())); + _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(err.to_string())).ser().body)); } }) .on_upgrade(|mut session| async move { @@ -135,7 +136,7 @@ where break; }; if let Err(err) = session.send(Message::Binary(incoming)).await { - _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())).ser())); + _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())).ser().body)); } }, outgoing = session.recv().fuse() => { @@ -159,7 +160,7 @@ where } Ok(_other) => {} Err(e) => { - _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())).ser())); + _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())).ser().body)); } } } diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index 749b290417..2e1815d841 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -1,6 +1,7 @@ use super::{Res, TryRes}; use crate::error::{ - FromServerFnError, ServerFnErrorWrapper, SERVER_FN_ERROR_HEADER, + FromServerFnError, ServerFnErrorResponseParts, ServerFnErrorWrapper, + SERVER_FN_ERROR_HEADER, }; use actix_web::{ http::{ @@ -72,20 +73,15 @@ where } impl Res for ActixResponse { - fn error_response(path: &str, err: Bytes) -> Self { + fn error_response(path: &str, err: ServerFnErrorResponseParts) -> Self { ActixResponse(SendWrapper::new( HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR) .append_header((SERVER_FN_ERROR_HEADER, path)) - .body(err), + .append_header((CONTENT_TYPE, err.content_type)) + .body(err.body), )) } - fn content_type(&mut self, content_type: &str) { - if let Ok(content_type) = HeaderValue::from_str(content_type) { - self.0.headers_mut().insert(CONTENT_TYPE, content_type); - } - } - fn redirect(&mut self, path: &str) { if let Ok(path) = HeaderValue::from_str(path) { *self.0.status_mut() = StatusCode::FOUND; diff --git a/server_fn/src/response/browser.rs b/server_fn/src/response/browser.rs index 0d9f5a0546..7e3ebf63dc 100644 --- a/server_fn/src/response/browser.rs +++ b/server_fn/src/response/browser.rs @@ -69,7 +69,8 @@ impl ClientRes for BrowserResponse { Err(E::from_server_fn_error(ServerFnErrorErr::Request( format!("{e:?}"), )) - .ser()) + .ser() + .body) } Ok(data) => { let data = data.unchecked_into::(); diff --git a/server_fn/src/response/generic.rs b/server_fn/src/response/generic.rs index 3ed01e62a2..f4cad60222 100644 --- a/server_fn/src/response/generic.rs +++ b/server_fn/src/response/generic.rs @@ -14,8 +14,8 @@ use super::{Res, TryRes}; use crate::error::{ - FromServerFnError, IntoAppError, ServerFnErrorErr, ServerFnErrorWrapper, - SERVER_FN_ERROR_HEADER, + FromServerFnError, IntoAppError, ServerFnErrorErr, + ServerFnErrorResponseParts, ServerFnErrorWrapper, SERVER_FN_ERROR_HEADER, }; use bytes::Bytes; use futures::{Stream, TryStreamExt}; @@ -92,21 +92,15 @@ where } impl Res for Response { - fn error_response(path: &str, err: Bytes) -> Self { + fn error_response(path: &str, err: ServerFnErrorResponseParts) -> Self { Response::builder() .status(http::StatusCode::INTERNAL_SERVER_ERROR) .header(SERVER_FN_ERROR_HEADER, path) - .body(err.into()) + .header(header::CONTENT_TYPE, err.content_type) + .body(err.body.into()) .unwrap() } - fn content_type(&mut self, content_type: &str) { - if let Ok(content_type) = HeaderValue::from_str(content_type) { - self.headers_mut() - .insert(header::CONTENT_TYPE, content_type); - } - } - fn redirect(&mut self, path: &str) { if let Ok(path) = HeaderValue::from_str(path) { self.headers_mut().insert(header::LOCATION, path); diff --git a/server_fn/src/response/http.rs b/server_fn/src/response/http.rs index e6f2842643..90dda289ac 100644 --- a/server_fn/src/response/http.rs +++ b/server_fn/src/response/http.rs @@ -1,7 +1,7 @@ use super::{Res, TryRes}; use crate::error::{ - FromServerFnError, IntoAppError, ServerFnErrorErr, ServerFnErrorWrapper, - SERVER_FN_ERROR_HEADER, + FromServerFnError, IntoAppError, ServerFnErrorErr, + ServerFnErrorResponseParts, ServerFnErrorWrapper, SERVER_FN_ERROR_HEADER, }; use axum::body::Body; use bytes::Bytes; @@ -16,7 +16,7 @@ where let builder = http::Response::builder(); builder .status(200) - .header(http::header::CONTENT_TYPE, content_type) + .header(header::CONTENT_TYPE, content_type) .body(Body::from(data)) .map_err(|e| { ServerFnErrorErr::Response(e.to_string()).into_app_error() @@ -27,7 +27,7 @@ where let builder = http::Response::builder(); builder .status(200) - .header(http::header::CONTENT_TYPE, content_type) + .header(header::CONTENT_TYPE, content_type) .body(Body::from(data)) .map_err(|e| { ServerFnErrorErr::Response(e.to_string()).into_app_error() @@ -43,7 +43,7 @@ where let builder = http::Response::builder(); builder .status(200) - .header(http::header::CONTENT_TYPE, content_type) + .header(header::CONTENT_TYPE, content_type) .body(body) .map_err(|e| { ServerFnErrorErr::Response(e.to_string()).into_app_error() @@ -52,21 +52,15 @@ where } impl Res for Response { - fn error_response(path: &str, err: Bytes) -> Self { + fn error_response(path: &str, err: ServerFnErrorResponseParts) -> Self { Response::builder() .status(http::StatusCode::INTERNAL_SERVER_ERROR) .header(SERVER_FN_ERROR_HEADER, path) - .body(err.into()) + .header(header::CONTENT_TYPE, err.content_type) + .body(err.body.into()) .unwrap() } - fn content_type(&mut self, content_type: &str) { - if let Ok(content_type) = HeaderValue::from_str(content_type) { - self.headers_mut() - .insert(header::CONTENT_TYPE, content_type); - } - } - fn redirect(&mut self, path: &str) { if let Ok(path) = HeaderValue::from_str(path) { self.headers_mut().insert(header::LOCATION, path); diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 3d303b7a56..326790f275 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -13,6 +13,7 @@ pub mod http; #[cfg(feature = "reqwest")] pub mod reqwest; +use crate::error::ServerFnErrorResponseParts; use bytes::Bytes; use futures::Stream; use std::future::Future; @@ -38,9 +39,7 @@ where /// Represents the response as created by the server; pub trait Res { /// Converts an error into a response, with a `500` status code and the error text as its body. - fn error_response(path: &str, err: Bytes) -> Self; - /// Set the content-type header for the response. - fn content_type(&mut self, #[allow(unused_variables)] content_type: &str) {} + fn error_response(path: &str, err: ServerFnErrorResponseParts) -> Self; /// Redirect the response by setting a 302 code and Location header. fn redirect(&mut self, path: &str); } @@ -100,11 +99,7 @@ impl TryRes for BrowserMockRes { } impl Res for BrowserMockRes { - fn error_response(_path: &str, _err: Bytes) -> Self { - unreachable!() - } - - fn content_type(&mut self, _content_type: &str) { + fn error_response(_path: &str, _err: ServerFnErrorResponseParts) -> Self { unreachable!() } From 6b6cafec43d7e3810be594fbaa0eb538dddfa2fd Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:06:56 -0700 Subject: [PATCH 05/18] Rename `BoxedService#ser` field and use bon-3.0.0 --- Cargo.toml | 2 +- server_fn/src/middleware/mod.rs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 27e4a7832d..36fef17b7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,7 +170,7 @@ async-lock = { default-features = false, version = "3.4.0" } base16 = { default-features = false, version = "0.2.1" } digest = { default-features = false, version = "0.10.7" } sha2 = { default-features = false, version = "0.10.8" } -bon = { default-features = false, version = "3.6.5" } +bon = { default-features = false, version = "3.0.0" } [profile.release] codegen-units = 1 diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 76e357a0ee..b457cd6b53 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -9,9 +9,10 @@ pub trait Layer: Send + Sync + 'static { } /// A type-erased service, which takes an HTTP request and returns a response. +#[non_exhaustive] pub struct BoxedService { /// A function that converts a [`ServerFnErrorErr`] into a string. - pub ser: ServerFnErrorSerializer, + pub err_ser: ServerFnErrorSerializer, /// The inner service. pub service: Box + Send>, } @@ -23,7 +24,7 @@ impl BoxedService { service: impl Service + Send + 'static, ) -> Self { Self { - ser, + err_ser: ser, service: Box::new(service), } } @@ -33,7 +34,7 @@ impl BoxedService { &mut self, req: Req, ) -> Pin + Send>> { - self.service.run(req, self.ser) + self.service.run(req, self.err_ser) } } @@ -104,7 +105,7 @@ mod axum { } fn call(&mut self, req: Request) -> Self::Future { - let inner = self.service.run(req, self.ser); + let inner = self.service.run(req, self.err_ser); Box::pin(async move { Ok(inner.await) }) } } @@ -121,7 +122,7 @@ mod axum { &self, inner: BoxedService, Response>, ) -> BoxedService, Response> { - BoxedService::new(inner.ser, self.layer(inner)) + BoxedService::new(inner.err_ser, self.layer(inner)) } } } From 43e76dea9b9e93c216ae82f064419e9991e88a66 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 00:18:24 +0000 Subject: [PATCH 06/18] [autofix.ci] apply automated fixes --- server_fn/src/middleware/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index b457cd6b53..988ac6d23e 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -129,9 +129,9 @@ mod axum { #[cfg(feature = "actix-no-default")] mod actix { - use crate::middleware::ServerFnErrorSerializer; use crate::{ error::ServerFnErrorErr, + middleware::ServerFnErrorSerializer, request::actix::ActixRequest, response::{actix::ActixResponse, Res}, }; From 9949f7998874b4b8e245b71eb1e066a2701bdfe3 Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Fri, 8 Aug 2025 09:39:19 -0700 Subject: [PATCH 07/18] Fix actix stream/websocket responses --- server_fn/src/request/actix.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server_fn/src/request/actix.rs b/server_fn/src/request/actix.rs index 03e5741694..6cac591939 100644 --- a/server_fn/src/request/actix.rs +++ b/server_fn/src/request/actix.rs @@ -107,6 +107,7 @@ where e.to_string(), )) .ser() + .body }) }); Ok(SendWrapper::new(stream)) @@ -143,7 +144,7 @@ where break; }; if let Err(err) = session.binary(incoming).await { - _ = response_stream_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())).ser())); + _ = response_stream_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())).ser().body)); } }, outgoing = msg_stream.next().fuse() => { @@ -171,7 +172,7 @@ where Ok(_other) => { } Err(e) => { - _ = response_stream_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())).ser())); + _ = response_stream_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())).ser().body)); } } } From fe088802295b3f1dea66bfe1dccde7becfbff8da Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Fri, 8 Aug 2025 10:07:01 -0700 Subject: [PATCH 08/18] Fix stream/websocket responses in a couple other places --- server_fn/src/client.rs | 3 ++- server_fn/src/response/reqwest.rs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server_fn/src/client.rs b/server_fn/src/client.rs index bfc9bb9733..1ed6f1ab8b 100644 --- a/server_fn/src/client.rs +++ b/server_fn/src/client.rs @@ -282,7 +282,8 @@ pub mod reqwest { Err(e) => Err(OutputStreamError::from_server_fn_error( ServerFnErrorErr::Request(e.to_string()), ) - .ser()), + .ser() + .body), }), write.with(|msg: Bytes| async move { Ok::< diff --git a/server_fn/src/response/reqwest.rs b/server_fn/src/response/reqwest.rs index 2a027ff8cb..0565ed4f21 100644 --- a/server_fn/src/response/reqwest.rs +++ b/server_fn/src/response/reqwest.rs @@ -24,6 +24,7 @@ impl ClientRes for Response { Ok(self.bytes_stream().map_err(|e| { E::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())) .ser() + .body })) } From 058a4fe73d62d9b3b80fcf31a7ae9f4fb7e41d82 Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:17:08 -0700 Subject: [PATCH 09/18] Replace `todo` doc comment with actual comment --- server_fn/src/middleware/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 988ac6d23e..6929484633 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -38,7 +38,7 @@ impl BoxedService { } } -/// todo +/// Type alias for the function that serializes a server fn error to [`ServerFnErrorResponseParts`]. pub type ServerFnErrorSerializer = fn(ServerFnErrorErr) -> ServerFnErrorResponseParts; From dda564b0f54d228d860730c645224f5f3a6f1c29 Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Tue, 19 Aug 2025 09:32:23 -0700 Subject: [PATCH 10/18] Update doc comments --- server_fn/src/error.rs | 2 +- server_fn/src/middleware/mod.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index ca1b76ffc2..760901b6b3 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -579,7 +579,7 @@ pub trait FromServerFnError: std::fmt::Debug + Sized + 'static { /// Converts a [`ServerFnErrorErr`] into the application-specific custom error type. fn from_server_fn_error(value: ServerFnErrorErr) -> Self; - /// Converts the custom error type to a [`String`]. + /// Converts the custom error type to [`ServerFnErrorResponseParts`]. fn ser(&self) -> ServerFnErrorResponseParts { let body = Self::Encoder::encode(self).unwrap_or_else(|e| { Self::Encoder::encode(&Self::from_server_fn_error( diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 6929484633..0ec2548d7f 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -11,7 +11,7 @@ pub trait Layer: Send + Sync + 'static { /// A type-erased service, which takes an HTTP request and returns a response. #[non_exhaustive] pub struct BoxedService { - /// A function that converts a [`ServerFnErrorErr`] into a string. + /// A function that converts a [`ServerFnErrorErr`] into [`ServerFnErrorResponseParts`]. pub err_ser: ServerFnErrorSerializer, /// The inner service. pub service: Box + Send>, @@ -38,7 +38,8 @@ impl BoxedService { } } -/// Type alias for the function that serializes a server fn error to [`ServerFnErrorResponseParts`]. +/// Type alias for a function that serializes a [`ServerFnErrorErr`] into +/// [`ServerFnErrorResponseParts`]. pub type ServerFnErrorSerializer = fn(ServerFnErrorErr) -> ServerFnErrorResponseParts; From 198824d1b4fa7310fe1b940d986e269c469d259a Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Sun, 24 Aug 2025 20:49:47 -0700 Subject: [PATCH 11/18] Revert "Update doc comments" This reverts commit dda564b0f54d228d860730c645224f5f3a6f1c29. --- server_fn/src/error.rs | 2 +- server_fn/src/middleware/mod.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index 760901b6b3..ca1b76ffc2 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -579,7 +579,7 @@ pub trait FromServerFnError: std::fmt::Debug + Sized + 'static { /// Converts a [`ServerFnErrorErr`] into the application-specific custom error type. fn from_server_fn_error(value: ServerFnErrorErr) -> Self; - /// Converts the custom error type to [`ServerFnErrorResponseParts`]. + /// Converts the custom error type to a [`String`]. fn ser(&self) -> ServerFnErrorResponseParts { let body = Self::Encoder::encode(self).unwrap_or_else(|e| { Self::Encoder::encode(&Self::from_server_fn_error( diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 0ec2548d7f..6929484633 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -11,7 +11,7 @@ pub trait Layer: Send + Sync + 'static { /// A type-erased service, which takes an HTTP request and returns a response. #[non_exhaustive] pub struct BoxedService { - /// A function that converts a [`ServerFnErrorErr`] into [`ServerFnErrorResponseParts`]. + /// A function that converts a [`ServerFnErrorErr`] into a string. pub err_ser: ServerFnErrorSerializer, /// The inner service. pub service: Box + Send>, @@ -38,8 +38,7 @@ impl BoxedService { } } -/// Type alias for a function that serializes a [`ServerFnErrorErr`] into -/// [`ServerFnErrorResponseParts`]. +/// Type alias for the function that serializes a server fn error to [`ServerFnErrorResponseParts`]. pub type ServerFnErrorSerializer = fn(ServerFnErrorErr) -> ServerFnErrorResponseParts; From 8f3e65b75ae3e109d9b108e0b58fdd20bb596f61 Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Sun, 24 Aug 2025 20:49:51 -0700 Subject: [PATCH 12/18] Revert "Replace `todo` doc comment with actual comment" This reverts commit 058a4fe73d62d9b3b80fcf31a7ae9f4fb7e41d82. --- server_fn/src/middleware/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 6929484633..988ac6d23e 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -38,7 +38,7 @@ impl BoxedService { } } -/// Type alias for the function that serializes a server fn error to [`ServerFnErrorResponseParts`]. +/// todo pub type ServerFnErrorSerializer = fn(ServerFnErrorErr) -> ServerFnErrorResponseParts; From 427258f524f6155fffe9422806b7991bb715c886 Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Sun, 24 Aug 2025 20:49:52 -0700 Subject: [PATCH 13/18] Revert "Fix stream/websocket responses in a couple other places" This reverts commit fe088802295b3f1dea66bfe1dccde7becfbff8da. --- server_fn/src/client.rs | 3 +-- server_fn/src/response/reqwest.rs | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/server_fn/src/client.rs b/server_fn/src/client.rs index 1ed6f1ab8b..bfc9bb9733 100644 --- a/server_fn/src/client.rs +++ b/server_fn/src/client.rs @@ -282,8 +282,7 @@ pub mod reqwest { Err(e) => Err(OutputStreamError::from_server_fn_error( ServerFnErrorErr::Request(e.to_string()), ) - .ser() - .body), + .ser()), }), write.with(|msg: Bytes| async move { Ok::< diff --git a/server_fn/src/response/reqwest.rs b/server_fn/src/response/reqwest.rs index 0565ed4f21..2a027ff8cb 100644 --- a/server_fn/src/response/reqwest.rs +++ b/server_fn/src/response/reqwest.rs @@ -24,7 +24,6 @@ impl ClientRes for Response { Ok(self.bytes_stream().map_err(|e| { E::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())) .ser() - .body })) } From d446fa21e9741e591d980d682ffc937d5b3c360c Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Sun, 24 Aug 2025 20:49:53 -0700 Subject: [PATCH 14/18] Revert "Fix actix stream/websocket responses" This reverts commit 9949f7998874b4b8e245b71eb1e066a2701bdfe3. --- server_fn/src/request/actix.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server_fn/src/request/actix.rs b/server_fn/src/request/actix.rs index 6cac591939..03e5741694 100644 --- a/server_fn/src/request/actix.rs +++ b/server_fn/src/request/actix.rs @@ -107,7 +107,6 @@ where e.to_string(), )) .ser() - .body }) }); Ok(SendWrapper::new(stream)) @@ -144,7 +143,7 @@ where break; }; if let Err(err) = session.binary(incoming).await { - _ = response_stream_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())).ser().body)); + _ = response_stream_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())).ser())); } }, outgoing = msg_stream.next().fuse() => { @@ -172,7 +171,7 @@ where Ok(_other) => { } Err(e) => { - _ = response_stream_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())).ser().body)); + _ = response_stream_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())).ser())); } } } From c2afd96d9af1e400c920cdeadba81084c858648a Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Sun, 24 Aug 2025 20:49:54 -0700 Subject: [PATCH 15/18] Revert "[autofix.ci] apply automated fixes" This reverts commit 43e76dea9b9e93c216ae82f064419e9991e88a66. --- server_fn/src/middleware/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 988ac6d23e..b457cd6b53 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -129,9 +129,9 @@ mod axum { #[cfg(feature = "actix-no-default")] mod actix { + use crate::middleware::ServerFnErrorSerializer; use crate::{ error::ServerFnErrorErr, - middleware::ServerFnErrorSerializer, request::actix::ActixRequest, response::{actix::ActixResponse, Res}, }; From 8a1f47446fc3311bc2ea65bef2225c6b1f435db1 Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Sun, 24 Aug 2025 20:49:55 -0700 Subject: [PATCH 16/18] Revert "Rename `BoxedService#ser` field and use bon-3.0.0" This reverts commit 6b6cafec43d7e3810be594fbaa0eb538dddfa2fd. --- Cargo.toml | 2 +- server_fn/src/middleware/mod.rs | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 36fef17b7b..27e4a7832d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,7 +170,7 @@ async-lock = { default-features = false, version = "3.4.0" } base16 = { default-features = false, version = "0.2.1" } digest = { default-features = false, version = "0.10.7" } sha2 = { default-features = false, version = "0.10.8" } -bon = { default-features = false, version = "3.0.0" } +bon = { default-features = false, version = "3.6.5" } [profile.release] codegen-units = 1 diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index b457cd6b53..76e357a0ee 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -9,10 +9,9 @@ pub trait Layer: Send + Sync + 'static { } /// A type-erased service, which takes an HTTP request and returns a response. -#[non_exhaustive] pub struct BoxedService { /// A function that converts a [`ServerFnErrorErr`] into a string. - pub err_ser: ServerFnErrorSerializer, + pub ser: ServerFnErrorSerializer, /// The inner service. pub service: Box + Send>, } @@ -24,7 +23,7 @@ impl BoxedService { service: impl Service + Send + 'static, ) -> Self { Self { - err_ser: ser, + ser, service: Box::new(service), } } @@ -34,7 +33,7 @@ impl BoxedService { &mut self, req: Req, ) -> Pin + Send>> { - self.service.run(req, self.err_ser) + self.service.run(req, self.ser) } } @@ -105,7 +104,7 @@ mod axum { } fn call(&mut self, req: Request) -> Self::Future { - let inner = self.service.run(req, self.err_ser); + let inner = self.service.run(req, self.ser); Box::pin(async move { Ok(inner.await) }) } } @@ -122,7 +121,7 @@ mod axum { &self, inner: BoxedService, Response>, ) -> BoxedService, Response> { - BoxedService::new(inner.err_ser, self.layer(inner)) + BoxedService::new(inner.ser, self.layer(inner)) } } } From b9c81d19a5b79f546ddac3932359efafa94dc028 Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Sun, 24 Aug 2025 20:49:56 -0700 Subject: [PATCH 17/18] Revert "Per PR suggestion, fix with an API-breaking change" This reverts commit a5078726f2cd9cf7e6f01fe65724d504caf39657. --- Cargo.lock | 73 ------------------------------- Cargo.toml | 1 - server_fn/Cargo.toml | 1 - server_fn/src/client.rs | 3 +- server_fn/src/codec/stream.rs | 4 +- server_fn/src/error.rs | 25 +++-------- server_fn/src/lib.rs | 26 +++++------ server_fn/src/middleware/mod.rs | 39 +++++++---------- server_fn/src/request/axum.rs | 7 ++- server_fn/src/response/actix.rs | 14 +++--- server_fn/src/response/browser.rs | 3 +- server_fn/src/response/generic.rs | 16 ++++--- server_fn/src/response/http.rs | 22 ++++++---- server_fn/src/response/mod.rs | 11 +++-- 14 files changed, 83 insertions(+), 162 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f30398ec6..5ec15a039a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -498,31 +498,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bon" -version = "3.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d9ef19ae5263a138da9a86871eca537478ab0332a7770bac7e3f08b801f89f" -dependencies = [ - "bon-macros", - "rustversion", -] - -[[package]] -name = "bon-macros" -version = "3.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577ae008f2ca11ca7641bd44601002ee5ab49ef0af64846ce1ab6057218a5cc1" -dependencies = [ - "darling", - "ident_case", - "prettyplease", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.104", -] - [[package]] name = "brotli" version = "8.0.1" @@ -853,41 +828,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "darling" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b136475da5ef7b6ac596c0e956e37bad51b85b987ff3d5e230e964936736b2" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b44ad32f92b75fb438b04b68547e521a548be8acc339a6dacc4a7121488f53e6" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.104", -] - -[[package]] -name = "darling_macro" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5be8a7a562d315a5b92a630c30cec6bcf663e6673f00fbb69cca66a6f521b9" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.104", -] - [[package]] name = "dashmap" version = "6.1.0" @@ -1715,12 +1655,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "1.0.3" @@ -3354,7 +3288,6 @@ dependencies = [ "actix-ws", "axum", "base64", - "bon", "bytes", "ciborium", "const-str", @@ -3555,12 +3488,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "subtle" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index 27e4a7832d..606e2b03bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,7 +170,6 @@ async-lock = { default-features = false, version = "3.4.0" } base16 = { default-features = false, version = "0.2.1" } digest = { default-features = false, version = "0.10.7" } sha2 = { default-features = false, version = "0.10.8" } -bon = { default-features = false, version = "3.6.5" } [profile.release] codegen-units = 1 diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 36988d0b1e..6b1e4f76c8 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -25,7 +25,6 @@ send_wrapper = { features = [ "futures", ], optional = true, workspace = true, default-features = true } thiserror = { workspace = true, default-features = true } -bon = { workspace = true } # registration system inventory = { optional = true, workspace = true, default-features = true } diff --git a/server_fn/src/client.rs b/server_fn/src/client.rs index bfc9bb9733..dbf974950b 100644 --- a/server_fn/src/client.rs +++ b/server_fn/src/client.rs @@ -141,8 +141,7 @@ pub mod browser { Err(OutputStreamError::from_server_fn_error( ServerFnErrorErr::Request(err.to_string()), ) - .ser() - .body) + .ser()) } }); let stream = SendWrapper::new(stream); diff --git a/server_fn/src/codec/stream.rs b/server_fn/src/codec/stream.rs index 9dadb2c644..a6c4183242 100644 --- a/server_fn/src/codec/stream.rs +++ b/server_fn/src/codec/stream.rs @@ -120,7 +120,7 @@ where async fn into_res(self) -> Result { Response::try_from_stream( Streaming::CONTENT_TYPE, - self.into_inner().map_err(|e| e.ser().body), + self.into_inner().map_err(|e| e.ser()), ) } } @@ -255,7 +255,7 @@ where Response::try_from_stream( Streaming::CONTENT_TYPE, self.into_inner() - .map(|stream| stream.map(Into::into).map_err(|e| e.ser().body)), + .map(|stream| stream.map(Into::into).map_err(|e| e.ser())), ) } } diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index ca1b76ffc2..c8944d7b8b 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -474,7 +474,7 @@ impl ServerFnUrlError { let mut url = Url::parse(base)?; url.query_pairs_mut() .append_pair("__path", &self.path) - .append_pair("__err", &URL_SAFE.encode(self.error.ser().body)); + .append_pair("__err", &URL_SAFE.encode(self.error.ser())); Ok(url) } @@ -536,7 +536,7 @@ impl Display for ServerFnErrorWrapper { write!( f, "{}", - ::into_encoded_string(self.0.ser().body) + ::into_encoded_string(self.0.ser()) ) } } @@ -560,17 +560,6 @@ impl FromStr for ServerFnErrorWrapper { } } -/// Response parts returned by [`FromServerFnError::ser`] to be returned to the client. -#[derive(bon::Builder)] -#[non_exhaustive] -pub struct ServerFnErrorResponseParts { - /// The raw [`Bytes`] of the serialized error. - pub body: Bytes, - /// The value of the `CONTENT_TYPE` associated constant for the `FromServerFnError` - /// implementation. Used to set the `content-type` header in http responses. - pub content_type: &'static str, -} - /// A trait for types that can be returned from a server function. pub trait FromServerFnError: std::fmt::Debug + Sized + 'static { /// The encoding strategy used to serialize and deserialize this error type. Must implement the [`Encodes`](server_fn::Encodes) trait for references to the error type. @@ -580,8 +569,8 @@ pub trait FromServerFnError: std::fmt::Debug + Sized + 'static { fn from_server_fn_error(value: ServerFnErrorErr) -> Self; /// Converts the custom error type to a [`String`]. - fn ser(&self) -> ServerFnErrorResponseParts { - let body = Self::Encoder::encode(self).unwrap_or_else(|e| { + fn ser(&self) -> Bytes { + Self::Encoder::encode(self).unwrap_or_else(|e| { Self::Encoder::encode(&Self::from_server_fn_error( ServerFnErrorErr::Serialization(e.to_string()), )) @@ -589,11 +578,7 @@ pub trait FromServerFnError: std::fmt::Debug + Sized + 'static { "error serializing should success at least with the \ Serialization error", ) - }); - ServerFnErrorResponseParts::builder() - .body(body) - .content_type(Self::Encoder::CONTENT_TYPE) - .build() + }) } /// Deserializes the custom error type from a [`&str`]. diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 211abcd570..da47ffcfd0 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -307,16 +307,18 @@ pub trait ServerFn: Send + Sized { .await .map(|res| (res, None)) .unwrap_or_else(|e| { - ( + let mut response = <::Server as crate::Server< Self::Error, Self::InputStreamError, Self::OutputStreamError, >>::Response::error_response( Self::PATH, e.ser() - ), - Some(e), - ) + ); + let content_type = + ::Encoder::CONTENT_TYPE; + response.content_type(content_type); + (response, Some(e)) }); // if it accepts HTML, we'll redirect to the Referer @@ -667,9 +669,8 @@ where ServerFnErrorErr::Serialization(e.to_string()), ) .ser() - .body }), - Err(err) => Err(err.ser().body), + Err(err) => Err(err.ser()), }; serialize_result(result) }); @@ -712,10 +713,9 @@ where ), ) .ser() - .body }) } - Err(err) => Err(err.ser().body), + Err(err) => Err(err.ser()), }; let result = serialize_result(result); if sink.send(result).await.is_err() { @@ -783,8 +783,7 @@ fn deserialize_result( return Err(E::from_server_fn_error( ServerFnErrorErr::Deserialization("Data is empty".into()), ) - .ser() - .body); + .ser()); } let tag = bytes[0]; @@ -796,8 +795,7 @@ fn deserialize_result( _ => Err(E::from_server_fn_error(ServerFnErrorErr::Deserialization( "Invalid data tag".into(), )) - .ser() - .body), // Invalid tag + .ser()), // Invalid tag } } @@ -887,7 +885,7 @@ pub struct ServerFnTraitObj { method: Method, handler: fn(Req) -> Pin + Send>>, middleware: fn() -> MiddlewareSet, - ser: middleware::ServerFnErrorSerializer, + ser: fn(ServerFnErrorErr) -> Bytes, } impl ServerFnTraitObj { @@ -963,7 +961,7 @@ where fn run( &mut self, req: Req, - _err_ser: middleware::ServerFnErrorSerializer, + _ser: fn(ServerFnErrorErr) -> Bytes, ) -> Pin + Send>> { let handler = self.handler; Box::pin(async move { handler(req).await }) diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 76e357a0ee..508ab76340 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -1,4 +1,5 @@ -use crate::error::{ServerFnErrorErr, ServerFnErrorResponseParts}; +use crate::error::ServerFnErrorErr; +use bytes::Bytes; use std::{future::Future, pin::Pin}; /// An abstraction over a middleware layer, which can be used to add additional @@ -11,7 +12,7 @@ pub trait Layer: Send + Sync + 'static { /// A type-erased service, which takes an HTTP request and returns a response. pub struct BoxedService { /// A function that converts a [`ServerFnErrorErr`] into a string. - pub ser: ServerFnErrorSerializer, + pub ser: fn(ServerFnErrorErr) -> Bytes, /// The inner service. pub service: Box + Send>, } @@ -19,7 +20,7 @@ pub struct BoxedService { impl BoxedService { /// Constructs a type-erased service from this service. pub fn new( - ser: ServerFnErrorSerializer, + ser: fn(ServerFnErrorErr) -> Bytes, service: impl Service + Send + 'static, ) -> Self { Self { @@ -37,25 +38,22 @@ impl BoxedService { } } -/// todo -pub type ServerFnErrorSerializer = - fn(ServerFnErrorErr) -> ServerFnErrorResponseParts; - /// A service converts an HTTP request into a response. pub trait Service { /// Converts a request into a response. fn run( &mut self, req: Request, - err_ser: ServerFnErrorSerializer, + ser: fn(ServerFnErrorErr) -> Bytes, ) -> Pin + Send>>; } #[cfg(feature = "axum-no-default")] mod axum { - use super::{BoxedService, ServerFnErrorSerializer, Service}; + use super::{BoxedService, Service}; use crate::{error::ServerFnErrorErr, response::Res, ServerFnError}; use axum::body::Body; + use bytes::Bytes; use http::{Request, Response}; use std::{future::Future, pin::Pin}; @@ -68,15 +66,14 @@ mod axum { fn run( &mut self, req: Request, - err_ser: ServerFnErrorSerializer, + ser: fn(ServerFnErrorErr) -> Bytes, ) -> Pin> + Send>> { let path = req.uri().path().to_string(); let inner = self.call(req); Box::pin(async move { inner.await.unwrap_or_else(|e| { - let err = err_ser(ServerFnErrorErr::MiddlewareError( - e.to_string(), - )); + let err = + ser(ServerFnErrorErr::MiddlewareError(e.to_string())); Response::::error_response(&path, err) }) }) @@ -128,13 +125,13 @@ mod axum { #[cfg(feature = "actix-no-default")] mod actix { - use crate::middleware::ServerFnErrorSerializer; use crate::{ error::ServerFnErrorErr, request::actix::ActixRequest, response::{actix::ActixResponse, Res}, }; use actix_web::{HttpRequest, HttpResponse}; + use bytes::Bytes; use std::{future::Future, pin::Pin}; impl super::Service for S @@ -146,15 +143,14 @@ mod actix { fn run( &mut self, req: HttpRequest, - err_ser: ServerFnErrorSerializer, + ser: fn(ServerFnErrorErr) -> Bytes, ) -> Pin + Send>> { let path = req.uri().path().to_string(); let inner = self.call(req); Box::pin(async move { inner.await.unwrap_or_else(|e| { - let err = err_ser(ServerFnErrorErr::MiddlewareError( - e.to_string(), - )); + let err = + ser(ServerFnErrorErr::MiddlewareError(e.to_string())); ActixResponse::error_response(&path, err).take() }) }) @@ -170,15 +166,14 @@ mod actix { fn run( &mut self, req: ActixRequest, - err_ser: ServerFnErrorSerializer, + ser: fn(ServerFnErrorErr) -> Bytes, ) -> Pin + Send>> { let path = req.0 .0.uri().path().to_string(); let inner = self.call(req.0.take().0); Box::pin(async move { ActixResponse::from(inner.await.unwrap_or_else(|e| { - let err = err_ser(ServerFnErrorErr::MiddlewareError( - e.to_string(), - )); + let err = + ser(ServerFnErrorErr::MiddlewareError(e.to_string())); ActixResponse::error_response(&path, err).take() })) }) diff --git a/server_fn/src/request/axum.rs b/server_fn/src/request/axum.rs index 4f41a7815d..1e6471ff6f 100644 --- a/server_fn/src/request/axum.rs +++ b/server_fn/src/request/axum.rs @@ -70,7 +70,6 @@ where e.to_string(), )) .ser() - .body }) })) } @@ -125,7 +124,7 @@ where .on_failed_upgrade({ let mut outgoing_tx = outgoing_tx.clone(); move |err: axum::Error| { - _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(err.to_string())).ser().body)); + _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(err.to_string())).ser())); } }) .on_upgrade(|mut session| async move { @@ -136,7 +135,7 @@ where break; }; if let Err(err) = session.send(Message::Binary(incoming)).await { - _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())).ser().body)); + _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())).ser())); } }, outgoing = session.recv().fuse() => { @@ -160,7 +159,7 @@ where } Ok(_other) => {} Err(e) => { - _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())).ser().body)); + _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())).ser())); } } } diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index 2e1815d841..749b290417 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -1,7 +1,6 @@ use super::{Res, TryRes}; use crate::error::{ - FromServerFnError, ServerFnErrorResponseParts, ServerFnErrorWrapper, - SERVER_FN_ERROR_HEADER, + FromServerFnError, ServerFnErrorWrapper, SERVER_FN_ERROR_HEADER, }; use actix_web::{ http::{ @@ -73,15 +72,20 @@ where } impl Res for ActixResponse { - fn error_response(path: &str, err: ServerFnErrorResponseParts) -> Self { + fn error_response(path: &str, err: Bytes) -> Self { ActixResponse(SendWrapper::new( HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR) .append_header((SERVER_FN_ERROR_HEADER, path)) - .append_header((CONTENT_TYPE, err.content_type)) - .body(err.body), + .body(err), )) } + fn content_type(&mut self, content_type: &str) { + if let Ok(content_type) = HeaderValue::from_str(content_type) { + self.0.headers_mut().insert(CONTENT_TYPE, content_type); + } + } + fn redirect(&mut self, path: &str) { if let Ok(path) = HeaderValue::from_str(path) { *self.0.status_mut() = StatusCode::FOUND; diff --git a/server_fn/src/response/browser.rs b/server_fn/src/response/browser.rs index 7e3ebf63dc..0d9f5a0546 100644 --- a/server_fn/src/response/browser.rs +++ b/server_fn/src/response/browser.rs @@ -69,8 +69,7 @@ impl ClientRes for BrowserResponse { Err(E::from_server_fn_error(ServerFnErrorErr::Request( format!("{e:?}"), )) - .ser() - .body) + .ser()) } Ok(data) => { let data = data.unchecked_into::(); diff --git a/server_fn/src/response/generic.rs b/server_fn/src/response/generic.rs index f4cad60222..3ed01e62a2 100644 --- a/server_fn/src/response/generic.rs +++ b/server_fn/src/response/generic.rs @@ -14,8 +14,8 @@ use super::{Res, TryRes}; use crate::error::{ - FromServerFnError, IntoAppError, ServerFnErrorErr, - ServerFnErrorResponseParts, ServerFnErrorWrapper, SERVER_FN_ERROR_HEADER, + FromServerFnError, IntoAppError, ServerFnErrorErr, ServerFnErrorWrapper, + SERVER_FN_ERROR_HEADER, }; use bytes::Bytes; use futures::{Stream, TryStreamExt}; @@ -92,15 +92,21 @@ where } impl Res for Response { - fn error_response(path: &str, err: ServerFnErrorResponseParts) -> Self { + fn error_response(path: &str, err: Bytes) -> Self { Response::builder() .status(http::StatusCode::INTERNAL_SERVER_ERROR) .header(SERVER_FN_ERROR_HEADER, path) - .header(header::CONTENT_TYPE, err.content_type) - .body(err.body.into()) + .body(err.into()) .unwrap() } + fn content_type(&mut self, content_type: &str) { + if let Ok(content_type) = HeaderValue::from_str(content_type) { + self.headers_mut() + .insert(header::CONTENT_TYPE, content_type); + } + } + fn redirect(&mut self, path: &str) { if let Ok(path) = HeaderValue::from_str(path) { self.headers_mut().insert(header::LOCATION, path); diff --git a/server_fn/src/response/http.rs b/server_fn/src/response/http.rs index 90dda289ac..e6f2842643 100644 --- a/server_fn/src/response/http.rs +++ b/server_fn/src/response/http.rs @@ -1,7 +1,7 @@ use super::{Res, TryRes}; use crate::error::{ - FromServerFnError, IntoAppError, ServerFnErrorErr, - ServerFnErrorResponseParts, ServerFnErrorWrapper, SERVER_FN_ERROR_HEADER, + FromServerFnError, IntoAppError, ServerFnErrorErr, ServerFnErrorWrapper, + SERVER_FN_ERROR_HEADER, }; use axum::body::Body; use bytes::Bytes; @@ -16,7 +16,7 @@ where let builder = http::Response::builder(); builder .status(200) - .header(header::CONTENT_TYPE, content_type) + .header(http::header::CONTENT_TYPE, content_type) .body(Body::from(data)) .map_err(|e| { ServerFnErrorErr::Response(e.to_string()).into_app_error() @@ -27,7 +27,7 @@ where let builder = http::Response::builder(); builder .status(200) - .header(header::CONTENT_TYPE, content_type) + .header(http::header::CONTENT_TYPE, content_type) .body(Body::from(data)) .map_err(|e| { ServerFnErrorErr::Response(e.to_string()).into_app_error() @@ -43,7 +43,7 @@ where let builder = http::Response::builder(); builder .status(200) - .header(header::CONTENT_TYPE, content_type) + .header(http::header::CONTENT_TYPE, content_type) .body(body) .map_err(|e| { ServerFnErrorErr::Response(e.to_string()).into_app_error() @@ -52,15 +52,21 @@ where } impl Res for Response { - fn error_response(path: &str, err: ServerFnErrorResponseParts) -> Self { + fn error_response(path: &str, err: Bytes) -> Self { Response::builder() .status(http::StatusCode::INTERNAL_SERVER_ERROR) .header(SERVER_FN_ERROR_HEADER, path) - .header(header::CONTENT_TYPE, err.content_type) - .body(err.body.into()) + .body(err.into()) .unwrap() } + fn content_type(&mut self, content_type: &str) { + if let Ok(content_type) = HeaderValue::from_str(content_type) { + self.headers_mut() + .insert(header::CONTENT_TYPE, content_type); + } + } + fn redirect(&mut self, path: &str) { if let Ok(path) = HeaderValue::from_str(path) { self.headers_mut().insert(header::LOCATION, path); diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 326790f275..3d303b7a56 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -13,7 +13,6 @@ pub mod http; #[cfg(feature = "reqwest")] pub mod reqwest; -use crate::error::ServerFnErrorResponseParts; use bytes::Bytes; use futures::Stream; use std::future::Future; @@ -39,7 +38,9 @@ where /// Represents the response as created by the server; pub trait Res { /// Converts an error into a response, with a `500` status code and the error text as its body. - fn error_response(path: &str, err: ServerFnErrorResponseParts) -> Self; + fn error_response(path: &str, err: Bytes) -> Self; + /// Set the content-type header for the response. + fn content_type(&mut self, #[allow(unused_variables)] content_type: &str) {} /// Redirect the response by setting a 302 code and Location header. fn redirect(&mut self, path: &str); } @@ -99,7 +100,11 @@ impl TryRes for BrowserMockRes { } impl Res for BrowserMockRes { - fn error_response(_path: &str, _err: ServerFnErrorResponseParts) -> Self { + fn error_response(_path: &str, _err: Bytes) -> Self { + unreachable!() + } + + fn content_type(&mut self, _content_type: &str) { unreachable!() } From 02a9810054e4c9d4bf0f6160c5153f68f6a5ca3b Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Sun, 24 Aug 2025 21:10:31 -0700 Subject: [PATCH 18/18] Revert commits to go back to the original semver-compatible approach Also adds some comments from @gbj's sample [PR](https://github.com/leptos-rs/leptos/pull/4247) --- server_fn/src/error.rs | 4 ++-- server_fn/src/middleware/mod.rs | 8 ++++++++ server_fn/src/response/mod.rs | 10 +++++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index c8944d7b8b..565bf98e79 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -568,7 +568,7 @@ pub trait FromServerFnError: std::fmt::Debug + Sized + 'static { /// Converts a [`ServerFnErrorErr`] into the application-specific custom error type. fn from_server_fn_error(value: ServerFnErrorErr) -> Self; - /// Converts the custom error type to a [`String`]. + /// Serializes the custom error type to bytes, according to the encoding given by `Self::Encoding`. fn ser(&self) -> Bytes { Self::Encoder::encode(self).unwrap_or_else(|e| { Self::Encoder::encode(&Self::from_server_fn_error( @@ -581,7 +581,7 @@ pub trait FromServerFnError: std::fmt::Debug + Sized + 'static { }) } - /// Deserializes the custom error type from a [`&str`]. + /// Deserializes the custom error type, according to the encoding given by `Self::Encoding`. fn de(data: Bytes) -> Self { Self::Encoder::decode(data).unwrap_or_else(|e| { ServerFnErrorErr::Deserialization(e.to_string()).into_app_error() diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 508ab76340..609e5646ad 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -72,6 +72,10 @@ mod axum { let inner = self.call(req); Box::pin(async move { inner.await.unwrap_or_else(|e| { + // TODO: This does not set the Content-Type on the response. Doing so will + // require a breaking change in order to get the correct encoding from the + // error's `FromServerFnError::Encoder::CONTENT_TYPE` impl. + // Note: This only applies to middleware errors. let err = ser(ServerFnErrorErr::MiddlewareError(e.to_string())); Response::::error_response(&path, err) @@ -149,6 +153,10 @@ mod actix { let inner = self.call(req); Box::pin(async move { inner.await.unwrap_or_else(|e| { + // TODO: This does not set the Content-Type on the response. Doing so will + // require a breaking change in order to get the correct encoding from the + // error's `FromServerFnError::Encoder::CONTENT_TYPE` impl. + // Note: This only applies to middleware errors. let err = ser(ServerFnErrorErr::MiddlewareError(e.to_string())); ActixResponse::error_response(&path, err).take() diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 3d303b7a56..41fa384542 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -37,10 +37,14 @@ where /// Represents the response as created by the server; pub trait Res { - /// Converts an error into a response, with a `500` status code and the error text as its body. + /// Converts an error into a response, with a `500` status code and the error as its body. fn error_response(path: &str, err: Bytes) -> Self; - /// Set the content-type header for the response. - fn content_type(&mut self, #[allow(unused_variables)] content_type: &str) {} + /// Set the `Content-Type` header for the response. + fn content_type(&mut self, #[allow(unused_variables)] content_type: &str) { + // TODO 0.9: remove this method and default implementation. It is only included here + // to allow setting the `Content-Type` header for error responses without requiring a + // semver-incompatible change. + } /// Redirect the response by setting a 302 code and Location header. fn redirect(&mut self, path: &str); }