Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9ac211f
fix: Set content type header for server function errors
spencewenski Aug 6, 2025
ed942bf
Use CONTENT_TYPE header from `actix` re-export
spencewenski Aug 6, 2025
bd317c8
Add `-` in doc comment
spencewenski Aug 6, 2025
a507872
Per PR suggestion, fix with an API-breaking change
spencewenski Aug 7, 2025
6b6cafe
Rename `BoxedService#ser` field and use bon-3.0.0
spencewenski Aug 8, 2025
43e76de
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 8, 2025
9949f79
Fix actix stream/websocket responses
spencewenski Aug 8, 2025
fe08880
Fix stream/websocket responses in a couple other places
spencewenski Aug 8, 2025
058a4fe
Replace `todo` doc comment with actual comment
spencewenski Aug 8, 2025
dda564b
Update doc comments
spencewenski Aug 19, 2025
198824d
Revert "Update doc comments"
spencewenski Aug 25, 2025
8f3e65b
Revert "Replace `todo` doc comment with actual comment"
spencewenski Aug 25, 2025
427258f
Revert "Fix stream/websocket responses in a couple other places"
spencewenski Aug 25, 2025
d446fa2
Revert "Fix actix stream/websocket responses"
spencewenski Aug 25, 2025
c2afd96
Revert "[autofix.ci] apply automated fixes"
spencewenski Aug 25, 2025
8a1f474
Revert "Rename `BoxedService#ser` field and use bon-3.0.0"
spencewenski Aug 25, 2025
b9c81d1
Revert "Per PR suggestion, fix with an API-breaking change"
spencewenski Aug 25, 2025
3758db9
Merge branch 'main' into server-fn-err-content-type-fix
spencewenski Aug 25, 2025
02a9810
Revert commits to go back to the original semver-compatible approach
spencewenski Aug 25, 2025
b03565e
Merge branch 'main' into server-fn-err-content-type-fix
spencewenski Aug 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions server_fn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ send_wrapper = { features = [
"futures",
], optional = true, workspace = true, default-features = true }
thiserror = { workspace = true, default-features = true }
bon = { workspace = true }
Copy link
Contributor Author

@spencewenski spencewenski Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be open to using typed-builder instead, or manually writing the necessary builder code.


# registration system
inventory = { optional = true, workspace = true, default-features = true }
Expand Down
6 changes: 4 additions & 2 deletions server_fn/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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::<
Expand Down
4 changes: 2 additions & 2 deletions server_fn/src/codec/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ where
async fn into_res(self) -> Result<Response, E> {
Response::try_from_stream(
Streaming::CONTENT_TYPE,
self.into_inner().map_err(|e| e.ser()),
self.into_inner().map_err(|e| e.ser().body),
)
}
}
Expand Down Expand Up @@ -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)),
)
}
}
Expand Down
27 changes: 21 additions & 6 deletions server_fn/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ impl<E: FromServerFnError> ServerFnUrlError<E> {
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)
}

Expand Down Expand Up @@ -536,7 +536,7 @@ impl<E: FromServerFnError> Display for ServerFnErrorWrapper<E> {
write!(
f,
"{}",
<E::Encoder as FormatType>::into_encoded_string(self.0.ser())
<E::Encoder as FormatType>::into_encoded_string(self.0.ser().body)
)
}
}
Expand All @@ -560,6 +560,17 @@ impl<E: FromServerFnError> FromStr for ServerFnErrorWrapper<E> {
}
}

/// 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.
Expand All @@ -568,17 +579,21 @@ 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()),
))
.expect(
"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`].
Expand Down
16 changes: 10 additions & 6 deletions server_fn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
});
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -781,7 +783,8 @@ fn deserialize_result<E: FromServerFnError>(
return Err(E::from_server_fn_error(
ServerFnErrorErr::Deserialization("Data is empty".into()),
)
.ser());
.ser()
.body);
}

let tag = bytes[0];
Expand All @@ -793,7 +796,8 @@ fn deserialize_result<E: FromServerFnError>(
_ => Err(E::from_server_fn_error(ServerFnErrorErr::Deserialization(
"Invalid data tag".into(),
))
.ser()), // Invalid tag
.ser()
.body), // Invalid tag
}
}

Expand Down Expand Up @@ -883,7 +887,7 @@ pub struct ServerFnTraitObj<Req, Res> {
method: Method,
handler: fn(Req) -> Pin<Box<dyn Future<Output = Res> + Send>>,
middleware: fn() -> MiddlewareSet<Req, Res>,
ser: fn(ServerFnErrorErr) -> Bytes,
ser: middleware::ServerFnErrorSerializer,
}

impl<Req, Res> ServerFnTraitObj<Req, Res> {
Expand Down Expand Up @@ -959,7 +963,7 @@ where
fn run(
&mut self,
req: Req,
_ser: fn(ServerFnErrorErr) -> Bytes,
_err_ser: middleware::ServerFnErrorSerializer,
) -> Pin<Box<dyn Future<Output = Res> + Send>> {
let handler = self.handler;
Box::pin(async move { handler(req).await })
Expand Down
Loading
Loading