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..36fef17b7b 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.0.0" } [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..1ed6f1ab8b 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); @@ -281,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/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..760901b6b3 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. @@ -568,9 +579,9 @@ 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`]. - fn ser(&self) -> Bytes { - Self::Encoder::encode(self).unwrap_or_else(|e| { + /// 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( 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 612b254c86..211abcd570 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -667,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) }); @@ -711,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() { @@ -781,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]; @@ -793,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 } } @@ -883,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 { @@ -959,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..0ec2548d7f 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 @@ -10,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: fn(ServerFnErrorErr) -> Bytes, + /// A function that converts a [`ServerFnErrorErr`] into [`ServerFnErrorResponseParts`]. + pub err_ser: ServerFnErrorSerializer, /// The inner service. pub service: Box + Send>, } @@ -20,11 +20,11 @@ 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 { - ser, + err_ser: ser, service: Box::new(service), } } @@ -34,26 +34,30 @@ impl BoxedService { &mut self, req: Req, ) -> Pin + Send>> { - self.service.run(req, self.ser) + self.service.run(req, self.err_ser) } } +/// Type alias for a function that serializes a [`ServerFnErrorErr`] into +/// [`ServerFnErrorResponseParts`]. +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 +70,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) }) }) @@ -101,7 +106,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) }) } } @@ -118,7 +123,7 @@ mod axum { &self, inner: BoxedService, Response>, ) -> BoxedService, Response> { - BoxedService::new(inner.ser, self.layer(inner)) + BoxedService::new(inner.err_ser, self.layer(inner)) } } } @@ -127,11 +132,11 @@ mod axum { mod actix { use crate::{ error::ServerFnErrorErr, + middleware::ServerFnErrorSerializer, 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 +148,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 +172,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/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)); } } } 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 c823c6ac9e..2e1815d841 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -1,11 +1,12 @@ use super::{Res, TryRes}; use crate::error::{ - FromServerFnError, ServerFnErrorWrapper, SERVER_FN_ERROR_HEADER, + FromServerFnError, ServerFnErrorResponseParts, ServerFnErrorWrapper, + SERVER_FN_ERROR_HEADER, }; use actix_web::{ http::{ header, - header::{HeaderValue, LOCATION}, + header::{HeaderValue, CONTENT_TYPE, LOCATION}, StatusCode, }, HttpResponse, @@ -72,11 +73,12 @@ 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), )) } 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 6952dd1e81..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,11 +92,12 @@ 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() } diff --git a/server_fn/src/response/http.rs b/server_fn/src/response/http.rs index 65e1b41832..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,11 +52,12 @@ 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() } diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 224713c149..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,8 +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; - + 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); } @@ -99,7 +99,7 @@ impl TryRes for BrowserMockRes { } impl Res for BrowserMockRes { - fn error_response(_path: &str, _err: Bytes) -> Self { + fn error_response(_path: &str, _err: ServerFnErrorResponseParts) -> Self { unreachable!() } 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 })) }