feat: support aliased Result return types for server_fn

- Enhanced error messages for better clarity when server functions do not meet the expected return type requirements.
- Added `trybuild` tests to ensure behaviour is not broken in future releases.
This commit is contained in:
Ifiok Jr.
2025-03-22 01:18:32 +00:00
parent 0a13ebd94d
commit 72e84f4e38
27 changed files with 544 additions and 25 deletions

1
Cargo.lock generated
View File

@@ -3279,6 +3279,7 @@ dependencies = [
"tokio-tungstenite",
"tower",
"tower-layer",
"trybuild",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",

View File

@@ -73,6 +73,7 @@ server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-beta" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-beta" }
tachys = { path = "./tachys", version = "0.2.0-beta" }
wasm-bindgen = { version = "0.2.100" }
trybuild = "1"
[profile.release]
codegen-units = 1

View File

@@ -32,7 +32,7 @@ tracing = { version = "0.1.41", optional = true }
[dev-dependencies]
log = "0.4.22"
typed-builder = "0.20.0"
trybuild = "1.0"
trybuild = { workspace = true }
leptos = { path = "../leptos" }
leptos_router = { path = "../router", features = ["ssr"] }
server_fn = { path = "../server_fn", features = ["cbor"] }
@@ -58,7 +58,7 @@ generic = ["server_fn_macro/generic"]
# https://github.com/rust-lang/cargo/issues/4423
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
# This is automatic as long as the leptos crate is depended upon,
# This is automatic as long as the leptos crate is depended upon,
# downstream usage should never manually enable this feature.
__internal_erase_components = []

View File

@@ -81,6 +81,9 @@ url = "2"
pin-project-lite = "0.2.15"
tokio = { version = "1.43.0", features = ["rt"], optional = true }
[dev-dependencies]
trybuild = { workspace = true }
[features]
axum-no-default = [
"ssr",

View File

@@ -559,6 +559,28 @@ where
}
}
#[doc(hidden)]
#[diagnostic::on_unimplemented(
message = "{Self} is not a `Result` or aliased `Result`. Server functions \
must return a `Result` or aliased `Result`.",
label = "Must return a `Result` or aliased `Result`.",
note = "If you are trying to return an alias of `Result`, you must also \
implement `FromServerFnError` for the error type."
)]
/// A trait for extracting the error and ok types from a [`Result`]. This is used to allow alias types to be returned from server functions.
pub trait ServerFnMustReturnResult {
/// The error type of the [`Result`].
type Err;
/// The ok type of the [`Result`].
type Ok;
}
#[doc(hidden)]
impl<T, E> ServerFnMustReturnResult for Result<T, E> {
type Err = E;
type Ok = T;
}
#[test]
fn assert_from_server_fn_error_impl() {
fn assert_impl<T: FromServerFnError>() {}

View File

@@ -0,0 +1,16 @@
use server_fn_macro_default::server;
#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)]
pub enum InvalidError {
#[error("error a")]
A,
}
type FullAlias = Result<String, InvalidError>;
#[server]
pub async fn full_alias_result() -> FullAlias {
Ok("hello".to_string())
}
fn main() {}

View File

@@ -0,0 +1,51 @@
error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied
--> tests/invalid/aliased_return_full.rs:11:1
|
11 | #[server]
| ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError`
|
= help: the trait `FromServerFnError` is implemented for `ServerFnError<CustErr>`
= note: required for `BrowserClient` to implement `Client<InvalidError>`
note: required by a bound in `server_fn::ServerFn::Client`
--> src/lib.rs
|
| type Client: Client<Self::Error>;
| ^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Client`
= note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied
--> tests/invalid/aliased_return_full.rs:11:1
|
11 | #[server]
| ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError`
|
= help: the trait `FromServerFnError` is implemented for `ServerFnError<CustErr>`
= note: required for `BrowserClient` to implement `Client<InvalidError>`
= note: required for `Http<PostUrl, Post<JsonEncoding>>` to implement `Protocol<FullAliasResult, std::string::String, BrowserClient, BrowserMockServer, InvalidError>`
note: required by a bound in `server_fn::ServerFn::Protocol`
--> src/lib.rs
|
| type Protocol: Protocol<
| ____________________^
| | Self,
| | Self::Output,
| | Self::Client,
| | Self::Server,
| | Self::Error,
| | >;
| |_____^ required by this bound in `ServerFn::Protocol`
= note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied
--> tests/invalid/aliased_return_full.rs:11:1
|
11 | #[server]
| ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError`
|
= help: the trait `FromServerFnError` is implemented for `ServerFnError<CustErr>`
note: required by a bound in `server_fn::ServerFn::Error`
--> src/lib.rs
|
| type Error: FromServerFnError + Send + Sync;
| ^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Error`
= note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)

View File

@@ -0,0 +1,14 @@
use server_fn_macro_default::server;
#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)]
pub enum InvalidError {
#[error("error a")]
A,
}
#[server]
pub async fn no_alias_result() -> Result<String, InvalidError> {
Ok("hello".to_string())
}
fn main() {}

View File

@@ -0,0 +1,50 @@
error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied
--> tests/invalid/aliased_return_none.rs:9:1
|
9 | #[server]
| ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError`
|
= help: the trait `FromServerFnError` is implemented for `ServerFnError<CustErr>`
= note: required for `BrowserClient` to implement `Client<InvalidError>`
note: required by a bound in `server_fn::ServerFn::Client`
--> src/lib.rs
|
| type Client: Client<Self::Error>;
| ^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Client`
= note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied
--> tests/invalid/aliased_return_none.rs:9:1
|
9 | #[server]
| ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError`
|
= help: the trait `FromServerFnError` is implemented for `ServerFnError<CustErr>`
= note: required for `BrowserClient` to implement `Client<InvalidError>`
= note: required for `Http<PostUrl, Post<JsonEncoding>>` to implement `Protocol<NoAliasResult, std::string::String, BrowserClient, BrowserMockServer, InvalidError>`
note: required by a bound in `server_fn::ServerFn::Protocol`
--> src/lib.rs
|
| type Protocol: Protocol<
| ____________________^
| | Self,
| | Self::Output,
| | Self::Client,
| | Self::Server,
| | Self::Error,
| | >;
| |_____^ required by this bound in `ServerFn::Protocol`
= note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied
--> tests/invalid/aliased_return_none.rs:10:50
|
10 | pub async fn no_alias_result() -> Result<String, InvalidError> {
| ^^^^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError`
|
= help: the trait `FromServerFnError` is implemented for `ServerFnError<CustErr>`
note: required by a bound in `server_fn::ServerFn::Error`
--> src/lib.rs
|
| type Error: FromServerFnError + Send + Sync;
| ^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Error`

View File

@@ -0,0 +1,16 @@
use server_fn_macro_default::server;
#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)]
pub enum InvalidError {
#[error("error a")]
A,
}
type PartAlias<T> = Result<T, InvalidError>;
#[server]
pub async fn part_alias_result() -> PartAlias<String> {
Ok("hello".to_string())
}
fn main() {}

View File

@@ -0,0 +1,51 @@
error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied
--> tests/invalid/aliased_return_part.rs:11:1
|
11 | #[server]
| ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError`
|
= help: the trait `FromServerFnError` is implemented for `ServerFnError<CustErr>`
= note: required for `BrowserClient` to implement `Client<InvalidError>`
note: required by a bound in `server_fn::ServerFn::Client`
--> src/lib.rs
|
| type Client: Client<Self::Error>;
| ^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Client`
= note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied
--> tests/invalid/aliased_return_part.rs:11:1
|
11 | #[server]
| ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError`
|
= help: the trait `FromServerFnError` is implemented for `ServerFnError<CustErr>`
= note: required for `BrowserClient` to implement `Client<InvalidError>`
= note: required for `Http<PostUrl, Post<JsonEncoding>>` to implement `Protocol<PartAliasResult, std::string::String, BrowserClient, BrowserMockServer, InvalidError>`
note: required by a bound in `server_fn::ServerFn::Protocol`
--> src/lib.rs
|
| type Protocol: Protocol<
| ____________________^
| | Self,
| | Self::Output,
| | Self::Client,
| | Self::Server,
| | Self::Error,
| | >;
| |_____^ required by this bound in `ServerFn::Protocol`
= note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied
--> tests/invalid/aliased_return_part.rs:11:1
|
11 | #[server]
| ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError`
|
= help: the trait `FromServerFnError` is implemented for `ServerFnError<CustErr>`
note: required by a bound in `server_fn::ServerFn::Error`
--> src/lib.rs
|
| type Error: FromServerFnError + Send + Sync;
| ^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Error`
= note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)

View File

@@ -0,0 +1,8 @@
use server_fn_macro_default::server;
#[server]
pub async fn empty_return() -> () {
()
}
fn main() {}

View File

@@ -0,0 +1,57 @@
error[E0277]: () is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
--> tests/invalid/empty_return.rs:3:1
|
3 | #[server]
| ^^^^^^^^^ Must return a `Result` or aliased `Result`.
|
= help: the trait `ServerFnMustReturnResult` is not implemented for `()`
= note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type.
= help: the trait `ServerFnMustReturnResult` is implemented for `Result<T, E>`
error[E0271]: expected `impl Future<Output = ()>` to be a future that resolves to `Result<_, _>`, but it resolves to `()`
--> tests/invalid/empty_return.rs:3:1
|
3 | #[server]
| ^^^^^^^^^ expected `Result<_, _>`, found `()`
|
= note: expected enum `Result<_, _>`
found unit type `()`
note: required by a bound in `ServerFn::{synthetic#0}`
--> src/lib.rs
|
| ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::{synthetic#0}`
error[E0277]: () is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
--> tests/invalid/empty_return.rs:3:1
|
3 | #[server]
| ^^^^^^^^^ Must return a `Result` or aliased `Result`.
|
= help: the trait `ServerFnMustReturnResult` is not implemented for `()`
= note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type.
= help: the trait `ServerFnMustReturnResult` is implemented for `Result<T, E>`
= note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: () is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
--> tests/invalid/empty_return.rs:3:1
|
3 | #[server]
| ^^^^^^^^^ Must return a `Result` or aliased `Result`.
|
= help: the trait `ServerFnMustReturnResult` is not implemented for `()`
= note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type.
= help: the trait `ServerFnMustReturnResult` is implemented for `Result<T, E>`
error[E0308]: mismatched types
--> tests/invalid/empty_return.rs:3:1
|
3 | #[server]
| ^^^^^^^^^ expected `()`, found `Result<_, _>`
|
= note: expected unit type `()`
found enum `Result<_, _>`
help: consider using `Result::expect` to unwrap the `Result<_, _>` value, panicking if the value is a `Result::Err`
|
3 | #[server].expect("REASON")
| +++++++++++++++++

View File

@@ -0,0 +1,6 @@
use server_fn_macro_default::server;
#[server]
pub async fn no_return() {}
fn main() {}

View File

@@ -0,0 +1,5 @@
error: expected `->`
--> tests/invalid/no_return.rs:4:26
|
4 | pub async fn no_return() {}
| ^

View File

@@ -0,0 +1,9 @@
use server_fn_macro_default::server;
use server_fn::error::ServerFnError;
#[server]
pub fn not_async() -> Result<String, ServerFnError> {
Ok("hello".to_string())
}
fn main() {}

View File

@@ -0,0 +1,13 @@
error: expected `async`
--> tests/invalid/not_async.rs:5:5
|
5 | pub fn not_async() -> Result<String, ServerFnError> {
| ^^
warning: unused import: `server_fn::error::ServerFnError`
--> tests/invalid/not_async.rs:2:5
|
2 | use server_fn::error::ServerFnError;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default

View File

@@ -0,0 +1,23 @@
use server_fn_macro_default::server;
use server_fn::error::{FromServerFnError, ServerFnErrorErr};
#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)]
pub enum CustomError {
#[error("error a")]
A,
#[error("error b")]
B,
}
impl FromServerFnError for CustomError {
fn from_server_fn_error(_: ServerFnErrorErr) -> Self {
Self::A
}
}
#[server]
pub async fn full_alias_result() -> CustomError {
CustomError::A
}
fn main() {}

View File

@@ -0,0 +1,57 @@
error[E0277]: CustomError is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
--> tests/invalid/not_result.rs:18:1
|
18 | #[server]
| ^^^^^^^^^ Must return a `Result` or aliased `Result`.
|
= help: the trait `ServerFnMustReturnResult` is not implemented for `CustomError`
= note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type.
= help: the trait `ServerFnMustReturnResult` is implemented for `Result<T, E>`
error[E0271]: expected `impl Future<Output = CustomError>` to be a future that resolves to `Result<_, _>`, but it resolves to `CustomError`
--> tests/invalid/not_result.rs:18:1
|
18 | #[server]
| ^^^^^^^^^ expected `Result<_, _>`, found `CustomError`
|
= note: expected enum `Result<_, _>`
found enum `CustomError`
note: required by a bound in `ServerFn::{synthetic#0}`
--> src/lib.rs
|
| ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::{synthetic#0}`
error[E0277]: CustomError is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
--> tests/invalid/not_result.rs:18:1
|
18 | #[server]
| ^^^^^^^^^ Must return a `Result` or aliased `Result`.
|
= help: the trait `ServerFnMustReturnResult` is not implemented for `CustomError`
= note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type.
= help: the trait `ServerFnMustReturnResult` is implemented for `Result<T, E>`
= note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: CustomError is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
--> tests/invalid/not_result.rs:18:1
|
18 | #[server]
| ^^^^^^^^^ Must return a `Result` or aliased `Result`.
|
= help: the trait `ServerFnMustReturnResult` is not implemented for `CustomError`
= note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type.
= help: the trait `ServerFnMustReturnResult` is implemented for `Result<T, E>`
error[E0308]: mismatched types
--> tests/invalid/not_result.rs:18:1
|
18 | #[server]
| ^^^^^^^^^ expected `CustomError`, found `Result<_, _>`
|
= note: expected enum `CustomError`
found enum `Result<_, _>`
help: consider using `Result::expect` to unwrap the `Result<_, _>` value, panicking if the value is a `Result::Err`
|
18 | #[server].expect("REASON")
| +++++++++++++++++

View File

@@ -0,0 +1,8 @@
#![cfg(feature = "browser")]
#[test]
fn aliased_results() {
let t = trybuild::TestCases::new();
t.pass("tests/valid/*.rs");
t.compile_fail("tests/invalid/*.rs")
}

View File

@@ -0,0 +1,11 @@
use server_fn_macro_default::server;
use server_fn::error::ServerFnError;
type FullAlias = Result<String, ServerFnError>;
#[server]
pub async fn full_alias_result() -> FullAlias {
Ok("hello".to_string())
}
fn main() {}

View File

@@ -0,0 +1,10 @@
use server_fn_macro_default::server;
use server_fn::error::ServerFnError;
#[server]
pub async fn no_alias_result() -> Result<String, ServerFnError> {
Ok("hello".to_string())
}
fn main() {}

View File

@@ -0,0 +1,11 @@
use server_fn_macro_default::server;
use server_fn::error::ServerFnError;
type PartAlias<T> = Result<T, ServerFnError>;
#[server]
pub async fn part_alias_result() -> PartAlias<String> {
Ok("hello".to_string())
}
fn main() {}

View File

@@ -0,0 +1,25 @@
use server_fn_macro_default::server;
use server_fn::error::{FromServerFnError, ServerFnErrorErr};
#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)]
pub enum CustomError {
#[error("error a")]
ErrorA,
#[error("error b")]
ErrorB,
}
impl FromServerFnError for CustomError {
fn from_server_fn_error(_: ServerFnErrorErr) -> Self {
Self::ErrorA
}
}
type FullAlias = Result<String, CustomError>;
#[server]
pub async fn full_alias_result() -> FullAlias {
Ok("hello".to_string())
}
fn main() {}

View File

@@ -0,0 +1,23 @@
use server_fn_macro_default::server;
use server_fn::error::{FromServerFnError, ServerFnErrorErr};
#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)]
pub enum CustomError {
#[error("error a")]
ErrorA,
#[error("error b")]
ErrorB,
}
impl FromServerFnError for CustomError {
fn from_server_fn_error(_: ServerFnErrorErr) -> Self {
Self::ErrorA
}
}
#[server]
pub async fn no_alias_result() -> Result<String, CustomError> {
Ok("hello".to_string())
}
fn main() {}

View File

@@ -0,0 +1,25 @@
use server_fn_macro_default::server;
use server_fn::error::{FromServerFnError, ServerFnErrorErr};
#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)]
pub enum CustomError {
#[error("error a")]
ErrorA,
#[error("error b")]
ErrorB,
}
impl FromServerFnError for CustomError {
fn from_server_fn_error(_: ServerFnErrorErr) -> Self {
Self::ErrorA
}
}
type PartAlias<T> = Result<T, CustomError>;
#[server]
pub async fn part_alias_result() -> PartAlias<String> {
Ok("hello".to_string())
}
fn main() {}

View File

@@ -551,16 +551,25 @@ impl ServerFnCall {
let protocol = self.protocol();
let middlewares = &self.body.middlewares;
let return_ty = &self.body.return_ty;
let output_ty = &self.body.output_ty;
let output_ty = self.body.output_ty
.as_ref()
.map_or_else(|| {
quote! {
<#return_ty as #server_fn_path::error::ServerFnMustReturnResult>::Ok
}
},
ToTokens::to_token_stream,
);
let error_ty = &self.body.error_ty;
let error_ty = error_ty
.as_ref()
.map(ToTokens::to_token_stream)
.unwrap_or_else(|| {
.map_or_else(|| {
quote! {
#server_fn_path::error::NoCustomError
<#return_ty as #server_fn_path::error::ServerFnMustReturnResult>::Err
}
});
},
ToTokens::to_token_stream,
);
let field_names = self.field_names();
// run_body in the trait implementation
@@ -877,7 +886,7 @@ impl Parse for Middleware {
}
}
fn output_type(return_ty: &Type) -> Result<&GenericArgument> {
fn output_type(return_ty: &Type) -> Option<&Type> {
if let syn::Type::Path(pat) = &return_ty {
if pat.path.segments[0].ident == "Result" {
if pat.path.segments.is_empty() {
@@ -885,19 +894,17 @@ fn output_type(return_ty: &Type) -> Result<&GenericArgument> {
} else if let PathArguments::AngleBracketed(args) =
&pat.path.segments[0].arguments
{
return Ok(&args.args[0]);
if let GenericArgument::Type(ty) = &args.args[0] {
return Some(ty);
}
}
}
};
Err(syn::Error::new(
return_ty.span(),
"server functions should return Result<T, E> where E: \
FromServerFnError",
))
None
}
fn err_type(return_ty: &Type) -> Result<Option<&Type>> {
fn err_type(return_ty: &Type) -> Option<&Type> {
if let syn::Type::Path(pat) = &return_ty {
if pat.path.segments[0].ident == "Result" {
if let PathArguments::AngleBracketed(args) =
@@ -905,21 +912,17 @@ fn err_type(return_ty: &Type) -> Result<Option<&Type>> {
{
// Result<T>
if args.args.len() == 1 {
return Ok(None);
return None;
}
// Result<T, _>
else if let GenericArgument::Type(ty) = &args.args[1] {
return Ok(Some(ty));
return Some(ty);
}
}
}
};
Err(syn::Error::new(
return_ty.span(),
"server functions should return Result<T, E> where E: \
FromServerFnError",
))
None
}
/// The arguments to the `server` macro.
@@ -1369,7 +1372,7 @@ pub struct ServerFnBody {
/// The return type of the server function.
pub return_ty: syn::Type,
/// The Ok output type of the server function.
pub output_ty: syn::GenericArgument,
pub output_ty: Option<syn::Type>,
/// The error output type of the server function.
pub error_ty: Option<syn::Type>,
/// The body of the server function.
@@ -1399,8 +1402,8 @@ impl Parse for ServerFnBody {
let output_arrow = input.parse()?;
let return_ty = input.parse()?;
let output_ty = output_type(&return_ty)?.clone();
let error_ty = err_type(&return_ty)?.cloned();
let output_ty = output_type(&return_ty).cloned();
let error_ty = err_type(&return_ty).cloned();
let block = input.parse()?;