diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index 46315b9d9..2419f6db2 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -885,9 +885,11 @@ pub fn CustomClientExample() -> impl IntoView { pub struct CustomClient; // Implement the `Client` trait for it. - impl Client for CustomClient + impl Client for CustomClient where E: FromServerFnError, + IS: FromServerFnError, + OS: FromServerFnError, { // BrowserRequest and BrowserResponse are the defaults used by other server functions. // They are wrappers for the underlying Web Fetch API types. @@ -904,7 +906,7 @@ pub fn CustomClientExample() -> impl IntoView { // modify the headers by appending one headers.append("X-Custom-Header", "foobar"); // delegate back out to BrowserClient to send the modified request - BrowserClient::send(req) + >::send(req) } fn open_websocket( @@ -912,8 +914,10 @@ pub fn CustomClientExample() -> impl IntoView { ) -> impl Future< Output = Result< ( - impl Stream> + Send + 'static, - impl Sink> + Send + 'static, + impl Stream> + + Send + + 'static, + impl Sink> + Send + 'static, ), E, >, diff --git a/flake.lock b/flake.lock index 9f283cc26..a53d300bc 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1726560853, - "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1727634051, - "narHash": "sha256-S5kVU7U82LfpEukbn/ihcyNt2+EvG7Z5unsKW9H/yFA=", + "lastModified": 1743583204, + "narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "06cf0e1da4208d3766d898b7fdab6513366d45b9", + "rev": "2c8d3f48d33929642c1c12cd243df4cc7d2ce434", "type": "github" }, "original": { @@ -36,11 +36,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1718428119, - "narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=", + "lastModified": 1736320768, + "narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5", + "rev": "4bc9c909d9ac828a039f288cf872d16d38185db8", "type": "github" }, "original": { @@ -62,11 +62,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1727749966, - "narHash": "sha256-DUS8ehzqB1DQzfZ4bRXVSollJhu+y7cvh1DJ9mbWebE=", + "lastModified": 1743820323, + "narHash": "sha256-UXxJogXhPhBFaX4uxmMudcD/x3sEGFtoSc4busTcftY=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "00decf1b4f9886d25030b9ee4aed7bfddccb5f66", + "rev": "b4734ce867252f92cdc7d25f8cc3b7cef153e703", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 3c56874cc..628039ec5 100644 --- a/flake.nix +++ b/flake.nix @@ -2,13 +2,20 @@ description = "A basic Rust devshell for NixOS users developing Leptos"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; rust-overlay.url = "github:oxalica/rust-overlay"; - flake-utils.url = "github:numtide/flake-utils"; + flake-utils.url = "github:numtide/flake-utils"; }; - outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }: - flake-utils.lib.eachDefaultSystem (system: + outputs = + { + nixpkgs, + rust-overlay, + flake-utils, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: let overlays = [ (import rust-overlay) ]; pkgs = import nixpkgs { @@ -18,22 +25,31 @@ with pkgs; { devShells.default = mkShell { - buildInputs = [ - openssl - pkg-config - cacert - cargo-make - trunk - (rust-bin.selectLatestNightlyWith( toolchain: toolchain.default.override { - extensions= [ "rust-src" "rust-analyzer" ]; - targets = [ "wasm32-unknown-unknown" ]; - })) - ] ++ pkgs.lib.optionals pkg.stdenv.isDarwin [ - darwin.apple_sdk.frameworks.SystemConfiguration - ]; + buildInputs = + [ + gcc + glib + openssl + pkg-config + cacert + cargo-make + trunk + (rust-bin.selectLatestNightlyWith ( + toolchain: + toolchain.default.override { + extensions = [ + "rust-src" + "rust-analyzer" + ]; + targets = [ "wasm32-unknown-unknown" ]; + } + )) + ] + ++ pkgs.lib.optionals pkg.stdenv.isDarwin [ + darwin.apple_sdk.frameworks.SystemConfiguration + ]; - shellHook = '' - ''; + shellHook = ''''; }; } ); diff --git a/leptos/src/form.rs b/leptos/src/form.rs index 553810883..dc0d3d526 100644 --- a/leptos/src/form.rs +++ b/leptos/src/form.rs @@ -97,6 +97,7 @@ where ServFn: Send + Sync + 'static, ServFn::Output: Send + Sync + 'static, ServFn::Error: Send + Sync + 'static, + ::Client: Client<::Error>, { // if redirect hook has not yet been set (by a router), defaults to a browser redirect _ = server_fn::redirect::set_redirect_hook(|loc: &str| { @@ -172,6 +173,7 @@ where ServFn::Error, >>::FormData: From, ServFn::Error: Send + Sync + 'static, + ::Client: Client<::Error>, { // if redirect hook has not yet been set (by a router), defaults to a browser redirect _ = server_fn::redirect::set_redirect_hook(|loc: &str| { diff --git a/server_fn/src/client.rs b/server_fn/src/client.rs index 70fc33a73..5c326a383 100644 --- a/server_fn/src/client.rs +++ b/server_fn/src/client.rs @@ -23,16 +23,16 @@ pub fn get_server_url() -> &'static str { /// This trait is implemented for things like a browser `fetch` request or for /// the `reqwest` trait. It should almost never be necessary to implement it /// yourself, unless you’re trying to use an alternative HTTP crate on the client side. -pub trait Client { +pub trait Client { /// The type of a request sent by this client. - type Request: ClientReq + Send + 'static; + type Request: ClientReq + Send + 'static; /// The type of a response received by this client. - type Response: ClientRes + Send + 'static; + type Response: ClientRes + Send + 'static; /// Sends the request and receives a response. fn send( req: Self::Request, - ) -> impl Future> + Send; + ) -> impl Future> + Send; /// Opens a websocket connection to the server. #[allow(clippy::type_complexity)] @@ -41,10 +41,12 @@ pub trait Client { ) -> impl Future< Output = Result< ( - impl Stream> + Send + 'static, - impl Sink> + Send + 'static, + impl Stream> + + Send + + 'static, + impl Sink> + Send + 'static, ), - E, + Error, >, > + Send; @@ -70,13 +72,19 @@ pub mod browser { /// Implements [`Client`] for a `fetch` request in the browser. pub struct BrowserClient; - impl Client for BrowserClient { + impl< + Error: FromServerFnError, + InputStreamError: FromServerFnError, + OutputStreamError: FromServerFnError, + > Client for BrowserClient + { type Request = BrowserRequest; type Response = BrowserResponse; fn send( req: Self::Request, - ) -> impl Future> + Send { + ) -> impl Future> + Send + { SendWrapper::new(async move { let req = req.0.take(); let RequestInner { @@ -106,10 +114,14 @@ pub mod browser { ) -> impl Future< Output = Result< ( - impl futures::Stream> + Send + 'static, - impl futures::Sink> + Send + 'static, + impl futures::Stream> + + Send + + 'static, + impl futures::Sink> + + Send + + 'static, ), - E, + Error, >, > + Send { SendWrapper::new(async move { @@ -117,18 +129,18 @@ pub mod browser { gloo_net::websocket::futures::WebSocket::open(url) .map_err(|err| { web_sys::console::error_1(&err.to_string().into()); - E::from_server_fn_error(ServerFnErrorErr::Request( - err.to_string(), - )) + Error::from_server_fn_error( + ServerFnErrorErr::Request(err.to_string()), + ) })?; let (sink, stream) = websocket.split(); let stream = stream .map_err(|err| { web_sys::console::error_1(&err.to_string().into()); - E::from_server_fn_error(ServerFnErrorErr::Request( - err.to_string(), - )) + OutputStreamError::from_server_fn_error( + ServerFnErrorErr::Request(err.to_string()), + ) }) .map_ok(move |msg| match msg { Message::Text(text) => Bytes::from(text), @@ -186,22 +198,26 @@ pub mod browser { } } - let sink = sink.with(|message: Result| async move { - match message { - Ok(message) => Ok(Message::Bytes(message.into())), - Err(err) => { - web_sys::console::error_1(&js_sys::JsString::from( - err.ser(), - )); - const CLOSE_CODE_ERROR: u16 = 1011; - Err(WebSocketError::ConnectionClose(CloseEvent { - code: CLOSE_CODE_ERROR, - reason: err.ser(), - was_clean: true, - })) + let sink = sink.with( + |message: Result| async move { + match message { + Ok(message) => Ok(Message::Bytes(message.into())), + Err(err) => { + web_sys::console::error_1( + &js_sys::JsString::from(err.ser()), + ); + const CLOSE_CODE_ERROR: u16 = 1011; + Err(WebSocketError::ConnectionClose( + CloseEvent { + code: CLOSE_CODE_ERROR, + reason: err.ser(), + was_clean: true, + }, + )) + } } - } - }); + }, + ); let sink = SendWrapperSink::new(Box::pin(sink)); Ok((stream, sink)) diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index dfb2a0c87..6bddc7f3c 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -174,9 +174,13 @@ pub use xxhash_rust; type ServerFnServerRequest = <::Server as crate::Server< ::Error, + ::InputStreamError, + ::OutputStreamError, >>::Request; type ServerFnServerResponse = <::Server as crate::Server< ::Error, + ::InputStreamError, + ::OutputStreamError, >>::Response; /// Defines a function that runs only on the server, but can be called from the server or the client. @@ -215,12 +219,20 @@ pub trait ServerFn: Send + Sized { /// The type of the HTTP client that will send the request from the client side. /// /// For example, this might be `gloo-net` in the browser, or `reqwest` for a desktop app. - type Client: Client; + type Client: Client< + Self::Error, + Self::InputStreamError, + Self::OutputStreamError, + >; /// The type of the HTTP server that will send the response from the server side. /// /// For example, this might be `axum` or `actix-web`. - type Server: Server; + type Server: Server< + Self::Error, + Self::InputStreamError, + Self::OutputStreamError, + >; /// The protocol the server function uses to communicate with the client. type Protocol: Protocol< @@ -229,6 +241,8 @@ pub trait ServerFn: Send + Sized { Self::Client, Self::Server, Self::Error, + Self::InputStreamError, + Self::OutputStreamError, >; /// The return type of the server function. @@ -237,8 +251,15 @@ pub trait ServerFn: Send + Sized { /// *from* `ClientResponse` when received by the client. type Output: Send; - /// The type of the error on the server function. Typically [`ServerFnError`], but allowed to be any type that implements [`FromServerFnError`]. + /// The type of error in the server function return. + /// Typically [`ServerFnError`], but allowed to be any type that implements [`FromServerFnError`]. type Error: FromServerFnError + Send + Sync; + /// The type of error in the server function for stream items sent from the client to the server. + /// Typically [`ServerFnError`], but allowed to be any type that implements [`FromServerFnError`]. + type InputStreamError: FromServerFnError + Send + Sync; + /// The type of error in the server function for stream items sent from the server to the client. + /// Typically [`ServerFnError`], but allowed to be any type that implements [`FromServerFnError`]. + type OutputStreamError: FromServerFnError + Send + Sync; /// Returns [`Self::PATH`]. fn url() -> &'static str { @@ -288,6 +309,8 @@ pub trait ServerFn: Send + Sized { ( <::Server as crate::Server< Self::Error, + Self::InputStreamError, + Self::OutputStreamError, >>::Response::error_response( Self::PATH, e.ser() ), @@ -331,10 +354,17 @@ pub trait ServerFn: Send + Sized { /// The protocol that a server function uses to communicate with the client. This trait handles /// the server and client side of running a server function. It is implemented for the [`Http`] and /// [`Websocket`] protocols and can be used to implement custom protocols. -pub trait Protocol -where - Server: crate::Server, - Client: crate::Client, +pub trait Protocol< + Input, + Output, + Client, + Server, + Error, + InputStreamError = Error, + OutputStreamError = Error, +> where + Server: crate::Server, + Client: crate::Client, { /// The HTTP method used for requests. const METHOD: Method; @@ -344,17 +374,17 @@ where fn run_server( request: Server::Request, server_fn: F, - ) -> impl Future> + Send + ) -> impl Future> + Send where F: Fn(Input) -> Fut + Send, - Fut: Future> + Send; + Fut: Future> + Send; /// Run the server function on the client. The implementation should handle serializing the /// input, sending the request, and deserializing the output. fn run_client( path: &str, input: Input, - ) -> impl Future> + Send; + ) -> impl Future> + Send; } /// The http protocol with specific input and output encodings for the request and response. This is @@ -561,18 +591,30 @@ impl< OutputEncoding, Client, Server, - E, - > Protocol, Client, Server, E> - for Websocket + Error, + InputStreamError, + OutputStreamError, + > + Protocol< + Input, + BoxedStream, + Client, + Server, + Error, + InputStreamError, + OutputStreamError, + > for Websocket where - Input: Deref> - + Into> - + From>, + Input: Deref> + + Into> + + From>, InputEncoding: Encodes + Decodes, OutputEncoding: Encodes + Decodes, - Server: crate::Server, - E: FromServerFnError + Send, - Client: crate::Client, + InputStreamError: FromServerFnError + Send, + OutputStreamError: FromServerFnError + Send, + Error: FromServerFnError + Send, + Server: crate::Server, + Client: crate::Client, OutputItem: Send + 'static, InputItem: Send + 'static, { @@ -581,34 +623,44 @@ where async fn run_server( request: Server::Request, server_fn: F, - ) -> Result + ) -> Result where F: Fn(Input) -> Fut + Send, - Fut: Future, E>> + Send, + Fut: Future< + Output = Result< + BoxedStream, + Error, + >, + > + Send, { let (request_bytes, response_stream, response) = request.try_into_websocket().await?; let input = request_bytes.map(|request_bytes| match request_bytes { Ok(request_bytes) => { InputEncoding::decode(request_bytes).map_err(|e| { - E::from_server_fn_error(ServerFnErrorErr::Deserialization( - e.to_string(), - )) + InputStreamError::from_server_fn_error( + ServerFnErrorErr::Deserialization(e.to_string()), + ) }) } Err(err) => Err(err), }); let boxed = Box::pin(input) - as Pin> + Send>>; + as Pin< + Box< + dyn Stream> + + Send, + >, + >; let input = BoxedStream { stream: boxed }; let output = server_fn(input.into()).await?; let output = output.stream.map(|output| match output { Ok(output) => OutputEncoding::encode(output).map_err(|e| { - E::from_server_fn_error(ServerFnErrorErr::Serialization( - e.to_string(), - )) + OutputStreamError::from_server_fn_error( + ServerFnErrorErr::Serialization(e.to_string()), + ) }), Err(err) => Err(err), }); @@ -629,8 +681,9 @@ where fn run_client( path: &str, input: Input, - ) -> impl Future, E>> + Send - { + ) -> impl Future< + Output = Result, Error>, + > + Send { let input = input.into(); async move { @@ -644,7 +697,7 @@ where if sink .send(input.and_then(|input| { InputEncoding::encode(input).map_err(|e| { - E::from_server_fn_error( + InputStreamError::from_server_fn_error( ServerFnErrorErr::Serialization( e.to_string(), ), @@ -663,14 +716,19 @@ where let stream = stream.map(|request_bytes| match request_bytes { Ok(request_bytes) => OutputEncoding::decode(request_bytes) .map_err(|e| { - E::from_server_fn_error( + OutputStreamError::from_server_fn_error( ServerFnErrorErr::Deserialization(e.to_string()), ) }), Err(err) => Err(err), }); let boxed = Box::pin(stream) - as Pin> + Send>>; + as Pin< + Box< + dyn Stream> + + Send, + >, + >; let output = BoxedStream { stream: boxed }; Ok(output) } @@ -738,13 +796,25 @@ impl ServerFnTraitObj { /// Converts the relevant parts of a server function into a trait object. pub const fn new< S: ServerFn< - Server: crate::Server, + Server: crate::Server< + S::Error, + S::InputStreamError, + S::OutputStreamError, + Request = Req, + Response = Res, + >, >, >( handler: fn(Req) -> Pin + Send>>, ) -> Self where - Req: crate::Req + Send + 'static, + Req: crate::Req< + S::Error, + S::InputStreamError, + S::OutputStreamError, + WebsocketResponse = Res, + > + Send + + 'static, Res: crate::TryRes + Send + 'static, { Self { @@ -848,14 +918,21 @@ pub mod axum { /// The axum server function backend pub struct AxumServerFnBackend; - impl Server for AxumServerFnBackend { + impl + Server + for AxumServerFnBackend + where + Error: FromServerFnError + Send + Sync, + InputStreamError: FromServerFnError + Send + Sync, + OutputStreamError: FromServerFnError + Send + Sync, + { type Request = Request; type Response = Response; #[allow(unused_variables)] fn spawn( future: impl Future + Send + 'static, - ) -> Result<(), E> { + ) -> Result<(), Error> { #[cfg(feature = "axum")] { tokio::spawn(future); @@ -863,7 +940,7 @@ pub mod axum { } #[cfg(not(feature = "axum"))] { - Err(E::from_server_fn_error( + Err(Error::from_server_fn_error( crate::error::ServerFnErrorErr::Request( "No async runtime available. You need to either \ enable the full axum feature to pull in tokio, or \ @@ -884,6 +961,8 @@ pub mod axum { T: ServerFn< Server: crate::Server< T::Error, + T::InputStreamError, + T::OutputStreamError, Request = Request, Response = Response, >, @@ -966,13 +1045,20 @@ pub mod actix { /// The actix server function backend pub struct ActixServerFnBackend; - impl Server for ActixServerFnBackend { + impl + Server + for ActixServerFnBackend + where + Error: FromServerFnError + Send + Sync, + InputStreamError: FromServerFnError + Send + Sync, + OutputStreamError: FromServerFnError + Send + Sync, + { type Request = ActixRequest; type Response = ActixResponse; fn spawn( future: impl Future + Send + 'static, - ) -> Result<(), E> { + ) -> Result<(), Error> { actix_web::rt::spawn(future); Ok(()) } @@ -986,6 +1072,8 @@ pub mod actix { T: ServerFn< Server: crate::Server< T::Error, + T::InputStreamError, + T::OutputStreamError, Request = ActixRequest, Response = ActixResponse, >, @@ -1075,13 +1163,20 @@ pub mod mock { /// server type when compiling for the client. pub struct BrowserMockServer; - impl crate::server::Server for BrowserMockServer { + impl + crate::server::Server + for BrowserMockServer + where + Error: Send + 'static, + InputStreamError: Send + 'static, + OutputStreamError: Send + 'static, + { type Request = crate::request::BrowserMockReq; type Response = crate::response::BrowserMockRes; fn spawn( _: impl Future + Send + 'static, - ) -> Result<(), E> { + ) -> Result<(), Error> { unreachable!() } } diff --git a/server_fn/src/request/actix.rs b/server_fn/src/request/actix.rs index 9e272b2d4..d72268c2b 100644 --- a/server_fn/src/request/actix.rs +++ b/server_fn/src/request/actix.rs @@ -38,9 +38,12 @@ impl From<(HttpRequest, Payload)> for ActixRequest { } } -impl Req for ActixRequest +impl + Req for ActixRequest where - E: FromServerFnError + Send, + Error: FromServerFnError + Send, + InputStreamError: FromServerFnError + Send, + OutputStreamError: FromServerFnError + Send, { type WebsocketResponse = ActixResponse; @@ -60,7 +63,9 @@ where self.header("Referer") } - fn try_into_bytes(self) -> impl Future> + Send { + fn try_into_bytes( + self, + ) -> impl Future> + Send { // Actix is going to keep this on a single thread anyway so it's fine to wrap it // with SendWrapper, which makes it `Send` but will panic if it moves to another thread SendWrapper::new(async move { @@ -72,18 +77,20 @@ where }) } - fn try_into_string(self) -> impl Future> + Send { + fn try_into_string( + self, + ) -> impl Future> + Send { // Actix is going to keep this on a single thread anyway so it's fine to wrap it // with SendWrapper, which makes it `Send` but will panic if it moves to another thread SendWrapper::new(async move { let payload = self.0.take().1; let bytes = payload.to_bytes().await.map_err(|e| { - E::from_server_fn_error(ServerFnErrorErr::Deserialization( + Error::from_server_fn_error(ServerFnErrorErr::Deserialization( e.to_string(), )) })?; String::from_utf8(bytes.into()).map_err(|e| { - E::from_server_fn_error(ServerFnErrorErr::Deserialization( + Error::from_server_fn_error(ServerFnErrorErr::Deserialization( e.to_string(), )) }) @@ -92,7 +99,7 @@ where fn try_into_stream( self, - ) -> Result> + Send, E> { + ) -> Result> + Send, Error> { let payload = self.0.take().1; let stream = payload.map(|res| { res.map_err(|e| { @@ -107,16 +114,16 @@ where self, ) -> Result< ( - impl Stream> + Send + 'static, - impl futures::Sink> + Send + 'static, + impl Stream> + Send + 'static, + impl futures::Sink> + Send + 'static, Self::WebsocketResponse, ), - E, + Error, > { let (request, payload) = self.0.take(); let (response, mut session, mut msg_stream) = actix_ws::handle(&request, payload).map_err(|e| { - E::from_server_fn_error(ServerFnErrorErr::Request( + Error::from_server_fn_error(ServerFnErrorErr::Request( e.to_string(), )) })?; @@ -124,7 +131,9 @@ where let (mut response_stream_tx, response_stream_rx) = futures::channel::mpsc::channel(2048); let (response_sink_tx, mut response_sink_rx) = - futures::channel::mpsc::channel(2048); + futures::channel::mpsc::channel::>( + 2048, + ); actix_web::rt::spawn(async move { loop { @@ -136,11 +145,11 @@ where match incoming { Ok(message) => { if let Err(err) = session.binary(message).await { - _ = response_stream_tx.start_send(Err(E::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())))); + _ = response_stream_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())))); } } Err(err) => { - _ = response_stream_tx.start_send(Err(err)); + _ = response_stream_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::ServerError(err.ser())))); } } }, @@ -166,7 +175,7 @@ where Ok(_other) => { } Err(e) => { - _ = response_stream_tx.start_send(Err(E::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())))); + _ = response_stream_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())))); } } } diff --git a/server_fn/src/request/axum.rs b/server_fn/src/request/axum.rs index f950d8724..645695cad 100644 --- a/server_fn/src/request/axum.rs +++ b/server_fn/src/request/axum.rs @@ -14,9 +14,12 @@ use http::{ use http_body_util::BodyExt; use std::borrow::Cow; -impl Req for Request +impl + Req for Request where - E: FromServerFnError + Send, + Error: FromServerFnError + Send, + InputStreamError: FromServerFnError + Send, + OutputStreamError: FromServerFnError + Send, { type WebsocketResponse = Response; @@ -42,7 +45,7 @@ where .map(|h| String::from_utf8_lossy(h.as_bytes())) } - async fn try_into_bytes(self) -> Result { + async fn try_into_bytes(self) -> Result { let (_parts, body) = self.into_parts(); body.collect().await.map(|c| c.to_bytes()).map_err(|e| { @@ -50,8 +53,8 @@ where }) } - async fn try_into_string(self) -> Result { - let bytes = self.try_into_bytes().await?; + async fn try_into_string(self) -> Result { + let bytes = Req::::try_into_bytes(self).await?; String::from_utf8(bytes.to_vec()).map_err(|e| { ServerFnErrorErr::Deserialization(e.to_string()).into_app_error() }) @@ -59,7 +62,8 @@ where fn try_into_stream( self, - ) -> Result> + Send + 'static, E> { + ) -> Result> + Send + 'static, Error> + { Ok(self.into_body().into_data_stream().map(|chunk| { chunk.map_err(|e| { ServerFnErrorErr::Deserialization(e.to_string()) @@ -72,22 +76,24 @@ where self, ) -> Result< ( - impl Stream> + Send + 'static, - impl Sink> + Send + 'static, + impl Stream> + Send + 'static, + impl Sink> + Send + 'static, Self::WebsocketResponse, ), - E, + Error, > { #[cfg(not(feature = "axum"))] { Err::< ( - futures::stream::Once>>, - futures::sink::Drain>, + futures::stream::Once< + std::future::Ready>, + >, + futures::sink::Drain>, Self::WebsocketResponse, ), - _, - >(E::from_server_fn_error( + Error, + >(Error::from_server_fn_error( crate::ServerFnErrorErr::Response( "Websocket connections not supported for Axum when the \ `axum` feature is not enabled on the `server_fn` crate." @@ -104,19 +110,21 @@ where axum::extract::ws::WebSocketUpgrade::from_request(self, &()) .await .map_err(|err| { - E::from_server_fn_error(ServerFnErrorErr::Request( + Error::from_server_fn_error(ServerFnErrorErr::Request( err.to_string(), )) })?; let (mut outgoing_tx, outgoing_rx) = futures::channel::mpsc::channel(2048); let (incoming_tx, mut incoming_rx) = - futures::channel::mpsc::channel::>(2048); + futures::channel::mpsc::channel::< + Result, + >(2048); let response = upgrade .on_failed_upgrade({ let mut outgoing_tx = outgoing_tx.clone(); move |err: axum::Error| { - _ = outgoing_tx.start_send(Err(E::from_server_fn_error(ServerFnErrorErr::Response(err.to_string())))); + _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(err.to_string())))); } }) .on_upgrade(|mut session| async move { @@ -129,11 +137,11 @@ where match incoming { Ok(message) => { if let Err(err) = session.send(Message::Binary(message)).await { - _ = outgoing_tx.start_send(Err(E::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())))); + _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())))); } } Err(err) => { - _ = outgoing_tx.start_send(Err(err)); + _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::ServerError(err.ser())))); } } }, @@ -153,7 +161,7 @@ where } Ok(_other) => {} Err(e) => { - _ = outgoing_tx.start_send(Err(E::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())))); + _ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())))); } } } diff --git a/server_fn/src/request/generic.rs b/server_fn/src/request/generic.rs index 55903e5ab..f5b12f3fc 100644 --- a/server_fn/src/request/generic.rs +++ b/server_fn/src/request/generic.rs @@ -24,17 +24,20 @@ use futures::{ use http::{Request, Response}; use std::borrow::Cow; -impl Req for Request +impl + Req for Request where - E: FromServerFnError + Send, + Error: FromServerFnError + Send, + InputStreamError: FromServerFnError + Send, + OutputStreamError: FromServerFnError + Send, { type WebsocketResponse = Response; - async fn try_into_bytes(self) -> Result { + async fn try_into_bytes(self) -> Result { Ok(self.into_body()) } - async fn try_into_string(self) -> Result { + async fn try_into_string(self) -> Result { String::from_utf8(self.into_body().into()).map_err(|err| { ServerFnErrorErr::Deserialization(err.to_string()).into_app_error() }) @@ -42,7 +45,8 @@ where fn try_into_stream( self, - ) -> Result> + Send + 'static, E> { + ) -> Result> + Send + 'static, Error> + { Ok(stream::iter(self.into_body()) .ready_chunks(16) .map(|chunk| Ok(Bytes::from(chunk)))) @@ -74,21 +78,25 @@ where self, ) -> Result< ( - impl Stream> + Send + 'static, - impl Sink> + Send + 'static, + impl Stream> + Send + 'static, + impl Sink> + Send + 'static, Self::WebsocketResponse, ), - E, + Error, > { Err::< ( - futures::stream::Once>>, - futures::sink::Drain>, + futures::stream::Once< + std::future::Ready>, + >, + futures::sink::Drain>, Self::WebsocketResponse, ), _, - >(E::from_server_fn_error(crate::ServerFnErrorErr::Response( - "Websockets are not supported on this platform.".to_string(), - ))) + >(Error::from_server_fn_error( + crate::ServerFnErrorErr::Response( + "Websockets are not supported on this platform.".to_string(), + ), + )) } } diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index ea7d42fad..b9ad66c8c 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -318,7 +318,7 @@ where } /// Represents the request as received by the server. -pub trait Req +pub trait Req where Self: Sized, { @@ -338,15 +338,19 @@ where fn referer(&self) -> Option>; /// Attempts to extract the body of the request into [`Bytes`]. - fn try_into_bytes(self) -> impl Future> + Send; + fn try_into_bytes( + self, + ) -> impl Future> + Send; /// Attempts to convert the body of the request into a string. - fn try_into_string(self) -> impl Future> + Send; + fn try_into_string( + self, + ) -> impl Future> + Send; /// Attempts to convert the body of the request into a stream of bytes. fn try_into_stream( self, - ) -> Result> + Send + 'static, E>; + ) -> Result> + Send + 'static, Error>; /// Attempts to convert the body of the request into a websocket handle. #[allow(clippy::type_complexity)] @@ -355,11 +359,11 @@ where ) -> impl Future< Output = Result< ( - impl Stream> + Send + 'static, - impl Sink> + Send + 'static, + impl Stream> + Send + 'static, + impl Sink> + Send + 'static, Self::WebsocketResponse, ), - E, + Error, >, > + Send; } @@ -368,7 +372,13 @@ where /// when compiling for the browser. pub struct BrowserMockReq; -impl Req for BrowserMockReq { +impl + Req for BrowserMockReq +where + Error: Send + 'static, + InputStreamError: Send + 'static, + OutputStreamError: Send + 'static, +{ type WebsocketResponse = crate::response::BrowserMockRes; fn as_query(&self) -> Option<&str> { @@ -386,17 +396,17 @@ impl Req for BrowserMockReq { fn referer(&self) -> Option> { unreachable!() } - async fn try_into_bytes(self) -> Result { + async fn try_into_bytes(self) -> Result { unreachable!() } - async fn try_into_string(self) -> Result { + async fn try_into_string(self) -> Result { unreachable!() } fn try_into_stream( self, - ) -> Result> + Send, E> { + ) -> Result> + Send, Error> { Ok(futures::stream::once(async { unreachable!() })) } @@ -404,17 +414,19 @@ impl Req for BrowserMockReq { self, ) -> Result< ( - impl Stream> + Send + 'static, - impl Sink> + Send + 'static, + impl Stream> + Send + 'static, + impl Sink> + Send + 'static, Self::WebsocketResponse, ), - E, + Error, > { #[allow(unreachable_code)] Err::< ( - futures::stream::Once>>, - futures::sink::Drain>, + futures::stream::Once< + std::future::Ready>, + >, + futures::sink::Drain>, Self::WebsocketResponse, ), _, diff --git a/server_fn/src/server.rs b/server_fn/src/server.rs index 7b9c24ef1..3ac9c797e 100644 --- a/server_fn/src/server.rs +++ b/server_fn/src/server.rs @@ -10,15 +10,21 @@ use std::future::Future; /// This trait is implemented for any server backend for server functions including /// `axum` and `actix-web`. It should almost never be necessary to implement it /// yourself, unless you’re trying to use an alternative HTTP server. -pub trait Server { +pub trait Server { /// The type of the HTTP request when received by the server function on the server side. - type Request: Req + Send + 'static; + type Request: Req< + Error, + InputStreamError, + OutputStreamError, + WebsocketResponse = Self::Response, + > + Send + + 'static; /// The type of the HTTP response returned by the server function on the server side. - type Response: Res + TryRes + Send + 'static; + type Response: Res + TryRes + Send + 'static; /// Spawn an async task on the server. fn spawn( future: impl Future + Send + 'static, - ) -> Result<(), E>; + ) -> Result<(), Error>; } diff --git a/server_fn/tests/invalid/aliased_return_full.stderr b/server_fn/tests/invalid/aliased_return_full.stderr index 8a55413b2..a621fdc23 100644 --- a/server_fn/tests/invalid/aliased_return_full.stderr +++ b/server_fn/tests/invalid/aliased_return_full.stderr @@ -9,8 +9,13 @@ error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied note: required by a bound in `server_fn::ServerFn::Client` --> src/lib.rs | - | type Client: Client; - | ^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Client` + | type Client: Client< + | __________________^ + | | Self::Error, + | | Self::InputStreamError, + | | Self::OutputStreamError, + | | >; + | |_____^ 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 @@ -30,8 +35,8 @@ note: required by a bound in `server_fn::ServerFn::Protocol` | | Self, | | Self::Output, | | Self::Client, - | | Self::Server, - | | Self::Error, +... | + | | Self::OutputStreamError, | | >; | |_____^ 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) @@ -49,3 +54,31 @@ note: required by a bound in `server_fn::ServerFn::Error` | 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) + +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` +note: required by a bound in `server_fn::ServerFn::InputStreamError` + --> src/lib.rs + | + | type InputStreamError: FromServerFnError + Send + Sync; + | ^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::InputStreamError` + = 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` +note: required by a bound in `server_fn::ServerFn::OutputStreamError` + --> src/lib.rs + | + | type OutputStreamError: FromServerFnError + Send + Sync; + | ^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::OutputStreamError` + = note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/server_fn/tests/invalid/aliased_return_none.stderr b/server_fn/tests/invalid/aliased_return_none.stderr index edfacac7e..520ec8655 100644 --- a/server_fn/tests/invalid/aliased_return_none.stderr +++ b/server_fn/tests/invalid/aliased_return_none.stderr @@ -9,8 +9,13 @@ error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied note: required by a bound in `server_fn::ServerFn::Client` --> src/lib.rs | - | type Client: Client; - | ^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Client` + | type Client: Client< + | __________________^ + | | Self::Error, + | | Self::InputStreamError, + | | Self::OutputStreamError, + | | >; + | |_____^ 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 @@ -30,8 +35,8 @@ note: required by a bound in `server_fn::ServerFn::Protocol` | | Self, | | Self::Output, | | Self::Client, - | | Self::Server, - | | Self::Error, +... | + | | Self::OutputStreamError, | | >; | |_____^ 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) @@ -48,3 +53,29 @@ note: required by a bound in `server_fn::ServerFn::Error` | | type Error: FromServerFnError + Send + Sync; | ^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Error` + +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 { + | ^^^^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError` + | + = help: the trait `FromServerFnError` is implemented for `ServerFnError` +note: required by a bound in `server_fn::ServerFn::InputStreamError` + --> src/lib.rs + | + | type InputStreamError: FromServerFnError + Send + Sync; + | ^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::InputStreamError` + +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 { + | ^^^^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError` + | + = help: the trait `FromServerFnError` is implemented for `ServerFnError` +note: required by a bound in `server_fn::ServerFn::OutputStreamError` + --> src/lib.rs + | + | type OutputStreamError: FromServerFnError + Send + Sync; + | ^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::OutputStreamError` diff --git a/server_fn/tests/invalid/aliased_return_part.stderr b/server_fn/tests/invalid/aliased_return_part.stderr index 634e58f73..e18024a9a 100644 --- a/server_fn/tests/invalid/aliased_return_part.stderr +++ b/server_fn/tests/invalid/aliased_return_part.stderr @@ -9,8 +9,13 @@ error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied note: required by a bound in `server_fn::ServerFn::Client` --> src/lib.rs | - | type Client: Client; - | ^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Client` + | type Client: Client< + | __________________^ + | | Self::Error, + | | Self::InputStreamError, + | | Self::OutputStreamError, + | | >; + | |_____^ 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 @@ -30,8 +35,8 @@ note: required by a bound in `server_fn::ServerFn::Protocol` | | Self, | | Self::Output, | | Self::Client, - | | Self::Server, - | | Self::Error, +... | + | | Self::OutputStreamError, | | >; | |_____^ 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) @@ -49,3 +54,31 @@ note: required by a bound in `server_fn::ServerFn::Error` | 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) + +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` +note: required by a bound in `server_fn::ServerFn::InputStreamError` + --> src/lib.rs + | + | type InputStreamError: FromServerFnError + Send + Sync; + | ^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::InputStreamError` + = 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` +note: required by a bound in `server_fn::ServerFn::OutputStreamError` + --> src/lib.rs + | + | type OutputStreamError: FromServerFnError + Send + Sync; + | ^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::OutputStreamError` + = note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 4be5beacd..a9efae510 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -570,6 +570,24 @@ impl ServerFnCall { }, ToTokens::to_token_stream, ); + let error_ws_in_ty = if self.websocket_protocol() { + self.body + .error_ws_in_ty + .as_ref() + .map(ToTokens::to_token_stream) + .unwrap_or(error_ty.clone()) + } else { + error_ty.clone() + }; + let error_ws_out_ty = if self.websocket_protocol() { + self.body + .error_ws_out_ty + .as_ref() + .map(ToTokens::to_token_stream) + .unwrap_or(error_ty.clone()) + } else { + error_ty.clone() + }; let field_names = self.field_names(); // run_body in the trait implementation @@ -645,6 +663,8 @@ impl ServerFnCall { type Protocol = #protocol; type Output = #output_ty; type Error = #error_ty; + type InputStreamError = #error_ws_in_ty; + type OutputStreamError = #error_ws_out_ty; fn middlewares() -> Vec>::Request, >::Response>>> { #middlewares @@ -925,6 +945,60 @@ fn err_type(return_ty: &Type) -> Option<&Type> { None } +fn err_ws_in_type( + inputs: &Punctuated, +) -> Option { + inputs.into_iter().find_map(|pat| { + if let syn::Type::Path(ref pat) = *pat.arg.ty { + if pat.path.segments[0].ident != "BoxedStream" { + return None; + } + + if let PathArguments::AngleBracketed(args) = + &pat.path.segments[0].arguments + { + // BoxedStream + if args.args.len() == 1 { + return None; + } + // BoxedStream + else if let GenericArgument::Type(ty) = &args.args[1] { + return Some(ty.clone()); + } + }; + }; + + None + }) +} + +fn err_ws_out_type(output_ty: &Option) -> Result> { + if let Some(syn::Type::Path(ref pat)) = output_ty { + if pat.path.segments[0].ident == "BoxedStream" { + if let PathArguments::AngleBracketed(args) = + &pat.path.segments[0].arguments + { + // BoxedStream + if args.args.len() == 1 { + return Ok(None); + } + // BoxedStream + else if let GenericArgument::Type(ty) = &args.args[1] { + return Ok(Some(ty.clone())); + } + + return Err(syn::Error::new( + output_ty.span(), + "websocket server functions should return \ + BoxedStream> where E: FromServerFnError", + )); + }; + } + }; + + Ok(None) +} + /// The arguments to the `server` macro. #[derive(Debug)] #[non_exhaustive] @@ -1375,6 +1449,10 @@ pub struct ServerFnBody { pub output_ty: Option, /// The error output type of the server function. pub error_ty: Option, + /// The error type of WebSocket client-sent error + pub error_ws_in_ty: Option, + /// The error type of WebSocket server-sent error + pub error_ws_out_ty: Option, /// The body of the server function. pub block: TokenStream2, /// The documentation of the server function. @@ -1404,6 +1482,8 @@ impl Parse for ServerFnBody { let return_ty = input.parse()?; let output_ty = output_type(&return_ty).cloned(); let error_ty = err_type(&return_ty).cloned(); + let error_ws_in_ty = err_ws_in_type(&inputs); + let error_ws_out_ty = err_ws_out_type(&output_ty)?; let block = input.parse()?; @@ -1461,6 +1541,8 @@ impl Parse for ServerFnBody { return_ty, output_ty, error_ty, + error_ws_in_ty, + error_ws_out_ty, block, attrs, docs,