diff --git a/integrations/actix/Cargo.toml b/integrations/actix/Cargo.toml index 7f09700fe..aafd39275 100644 --- a/integrations/actix/Cargo.toml +++ b/integrations/actix/Cargo.toml @@ -22,7 +22,6 @@ parking_lot = "0.12.1" regex = "1.7.0" tracing = "0.1.37" tokio = { version = "1", features = ["rt", "fs"] } -url = "2.5.0" [features] nonce = ["leptos/nonce"] diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 3cfce4126..e49c85c50 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -6,10 +6,7 @@ //! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples) //! directory in the Leptos repository. -use actix_http::{ - body::MessageBody, - header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER}, -}; +use actix_http::header::{HeaderName, HeaderValue, ACCEPT}; use actix_web::{ body::BoxBody, dev::{ServiceFactory, ServiceRequest}, @@ -28,24 +25,15 @@ use leptos_meta::*; use leptos_router::*; use parking_lot::RwLock; use regex::Regex; -use server_fn::{ - error::{ - NoCustomError, ServerFnErrorSerde, ServerFnUrlError, - SERVER_FN_ERROR_HEADER, - }, - redirect::REDIRECT_HEADER, - request::actix::ActixRequest, -}; +use server_fn::{redirect::REDIRECT_HEADER, request::actix::ActixRequest}; use std::{ fmt::{Debug, Display}, future::Future, pin::Pin, - str::FromStr, sync::Arc, }; #[cfg(debug_assertions)] use tracing::instrument; -use url::Url; /// This struct lets you define headers and override the status of the Response from an Element or a Server Function /// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses. #[derive(Debug, Clone, Default)] @@ -253,82 +241,12 @@ pub fn handle_server_fns_with_context( let res_parts = ResponseOptions::default(); provide_context(res_parts.clone()); - let accepts_html = req - .headers() - .get(ACCEPT) - .and_then(|v| v.to_str().ok()) - .map(|v| v.contains("text/html")) - .unwrap_or(false); - let referrer = req.headers().get(REFERER).cloned(); - let mut res = service .0 .run(ActixRequest::from((req, payload))) .await .take(); - // it it accepts text/html (i.e., is a plain form post) and doesn't already have a - // Location set, then redirect to to Referer - if accepts_html { - if let Some(mut referrer) = referrer { - let location = res.headers().get(LOCATION); - if location.is_none() { - let is_error = res.status() - == StatusCode::INTERNAL_SERVER_ERROR; - if is_error { - if let Some(Ok(path)) = res - .headers() - .get(SERVER_FN_ERROR_HEADER) - .map(|n| n.to_str().map(|n| n.to_owned())) - { - let (headers, body) = res.into_parts(); - if let Ok(body) = body.try_into_bytes() { - if let Ok(body) = - String::from_utf8(body.to_vec()) - { - // TODO allow other kinds? - let err: ServerFnError< - NoCustomError, - > = ServerFnErrorSerde::de(&body); - if let Ok(referrer_str) = - referrer.to_str() - { - let mut modified = - Url::parse(referrer_str) - .expect( - "couldn't parse \ - URL from Referer \ - header.", - ); - modified - .query_pairs_mut() - .append_pair( - "__path", - &path - ) - .append_pair( - "__err", - &ServerFnErrorSerde::ser(&err).unwrap_or_default() - ); - let modified = - HeaderValue::from_str( - modified.as_ref(), - ); - if let Ok(header) = modified { - referrer = header; - } - } - } - } - res = headers.set_body(BoxBody::new("")) - } - }; - *res.status_mut() = StatusCode::FOUND; - res.headers_mut().insert(LOCATION, referrer); - } - } - }; - // Override StatusCode if it was set in a Resource or Element if let Some(status) = res_parts.0.read().status { *res.status_mut() = status; diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index 730e7ff79..17f287f2a 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -20,7 +20,7 @@ typed-builder = "0.18" typed-builder-macro = "0.18" serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } -server_fn = { workspace = true, features = ["browser"] } +server_fn = { workspace = true, features = ["browser", "url", "cbor"] } web-sys = { version = "0.3.63", features = [ "ShadowRoot", "ShadowRootInit", diff --git a/leptos_server/src/action.rs b/leptos_server/src/action.rs index 15dc1e65e..b5ca4e5d1 100644 --- a/leptos_server/src/action.rs +++ b/leptos_server/src/action.rs @@ -3,10 +3,7 @@ use leptos_reactive::{ batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*, spawn_local, store_value, use_context, ReadSignal, RwSignal, StoredValue, }; -use server_fn::{ - error::{NoCustomError, ServerFnUrlError}, - ServerFn, ServerFnError, -}; +use server_fn::{error::ServerFnUrlError, ServerFn, ServerFnError}; use std::{cell::Cell, future::Future, pin::Pin, rc::Rc}; /// An action synchronizes an imperative `async` call to the synchronous reactive system. diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 36027b4ec..aa1394ceb 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -64,9 +64,10 @@ reqwest = { version = "0.11", default-features = false, optional = true, feature "multipart", "stream", ] } +url = "2" [features] -default = ["url", "json", "cbor"] +default = [ "json", "cbor"] actix = ["ssr", "dep:actix-web", "dep:send_wrapper"] axum = [ "ssr", diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index e47ff6e03..ab5b57fed 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -7,6 +7,7 @@ use std::{ sync::Arc, }; use thiserror::Error; +use url::Url; /// A custom header that can be used to indicate a server function returned an error. pub const SERVER_FN_ERROR_HEADER: &'static str = "serverfnerror"; @@ -403,46 +404,21 @@ impl From> for ServerFnErrorErr { } } -/// TODO: Write Documentation +/// Associates a particular server function error with the server function +/// found at a particular path. +/// +/// This can be used to pass an error from the server back to the client +/// without JavaScript/WASM supported, by encoding it in the URL as a qurey string. +/// This is useful for progressive enhancement. #[derive(Debug)] pub struct ServerFnUrlError { path: String, error: ServerFnError, } -impl FromStr for ServerFnUrlError -where - CustErr: FromStr + Display, -{ - type Err = (); - - fn from_str(s: &str) -> Result { - match s.split_once('|') { - None => Err(()), - Some((path, error)) => { - let error = ServerFnError::::de(error); - Ok(ServerFnUrlError { - path: path.to_string(), - error, - }) - } - } - } -} - -impl Display for ServerFnUrlError -where - CustErr: FromStr + Display, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}|", self.path)?; - write!(f, "{}", &self.error.ser()?)?; - Ok(()) - } -} - impl ServerFnUrlError { - /// TODO: Write Documentation + /// Creates a new structure associating the server function at some path + /// with a particular error. pub fn new(path: impl Display, error: ServerFnError) -> Self { Self { path: path.to_string(), @@ -450,15 +426,30 @@ impl ServerFnUrlError { } } - /// TODO: Write documentation + /// The error itself. pub fn error(&self) -> &ServerFnError { &self.error } - /// TODO: Add docs + /// The path of the server function that generated this error. pub fn path(&self) -> &str { &self.path } + + /// Adds an encoded form of this server function error to the given base URL. + pub fn to_url(&self, base: &str) -> Result + where + CustErr: FromStr + Display, + { + let mut url = Url::parse(base)?; + url.query_pairs_mut() + .append_pair("__path", &self.path) + .append_pair( + "__err", + &ServerFnErrorSerde::ser(&self.error).unwrap_or_default(), + ); + Ok(url) + } } impl From> for ServerFnError { diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 279a722d3..b6986020e 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -1,5 +1,4 @@ #![forbid(unsafe_code)] -// uncomment this if you want to feel pain #![deny(missing_docs)] //! # Server Functions @@ -126,7 +125,7 @@ use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; pub use const_format; use dashmap::DashMap; pub use error::ServerFnError; -use error::ServerFnErrorSerde; +use error::{ServerFnErrorSerde, ServerFnUrlError}; use http::Method; use middleware::{Layer, Service}; use once_cell::sync::Lazy; @@ -236,10 +235,42 @@ where fn run_on_server( req: Self::ServerRequest, ) -> impl Future + Send { - async { - Self::execute_on_server(req).await.unwrap_or_else(|e| { - Self::ServerResponse::error_response(Self::PATH, e) - }) + // Server functions can either be called by a real Client, + // or directly by an HTML
. If they're accessed by a , default to + // redirecting back to the Referer. + let accepts_html = req + .accepts() + .map(|n| n.contains("text/html")) + .unwrap_or(false); + let mut referer = req.referer().as_deref().map(ToOwned::to_owned); + + async move { + let (mut res, err) = Self::execute_on_server(req) + .await + .map(|res| (res, None)) + .unwrap_or_else(|e| { + ( + Self::ServerResponse::error_response(Self::PATH, &e), + Some(e), + ) + }); + + // if it accepts HTML, we'll redirect to the Referer + if accepts_html { + // if it had an error, encode that error in the URL + if let Some(err) = err { + if let Ok(url) = ServerFnUrlError::new(Self::PATH, err) + .to_url(referer.as_deref().unwrap_or("/")) + { + referer = Some(url.to_string()); + } + } + + // set the status code and Location header + res.redirect(referer.as_deref().unwrap_or("/")); + } + + res } } diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index ed537238c..9c23e8582 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -53,7 +53,7 @@ mod axum { Box::pin(async move { inner.await.unwrap_or_else(|e| { let err = ServerFnError::from(e); - Response::::error_response(&path, err) + Response::::error_response(&path, &err) }) }) } @@ -131,7 +131,7 @@ mod actix { Box::pin(async move { inner.await.unwrap_or_else(|e| { let err = ServerFnError::new(e); - ActixResponse::error_response(&path, err).take() + ActixResponse::error_response(&path, &err).take() }) }) } @@ -152,7 +152,7 @@ mod actix { Box::pin(async move { ActixResponse::from(inner.await.unwrap_or_else(|e| { let err = ServerFnError::new(e); - ActixResponse::error_response(&path, err).take() + ActixResponse::error_response(&path, &err).take() })) }) } diff --git a/server_fn/src/request/actix.rs b/server_fn/src/request/actix.rs index b0a727aac..05c801a19 100644 --- a/server_fn/src/request/actix.rs +++ b/server_fn/src/request/actix.rs @@ -3,7 +3,7 @@ use actix_web::{web::Payload, HttpRequest}; use bytes::Bytes; use futures::Stream; use send_wrapper::SendWrapper; -use std::future::Future; +use std::{borrow::Cow, future::Future}; /// A wrapped Actix request. /// @@ -17,6 +17,14 @@ impl ActixRequest { pub fn take(self) -> (HttpRequest, Payload) { self.0.take() } + + fn header(&self, name: &str) -> Option> { + self.0 + .0 + .headers() + .get(name) + .map(|h| String::from_utf8_lossy(h.as_bytes())) + } } impl From<(HttpRequest, Payload)> for ActixRequest { @@ -30,12 +38,16 @@ impl Req for ActixRequest { self.0 .0.uri().query() } - fn to_content_type(&self) -> Option { - self.0 - .0 - .headers() - .get("Content-Type") - .map(|h| String::from_utf8_lossy(h.as_bytes()).to_string()) + fn to_content_type(&self) -> Option> { + self.header("Content-Type") + } + + fn accepts(&self) -> Option> { + self.header("Accept") + } + + fn referer(&self) -> Option> { + self.header("Referer") } fn try_into_bytes( diff --git a/server_fn/src/request/axum.rs b/server_fn/src/request/axum.rs index 799c82ecc..690eb9e5d 100644 --- a/server_fn/src/request/axum.rs +++ b/server_fn/src/request/axum.rs @@ -1,18 +1,34 @@ use crate::{error::ServerFnError, request::Req}; use axum::body::{Body, Bytes}; use futures::{Stream, StreamExt}; -use http::{header::CONTENT_TYPE, Request}; +use http::{ + header::{ACCEPT, CONTENT_TYPE, REFERER}, + Request, +}; use http_body_util::BodyExt; +use std::borrow::Cow; impl Req for Request { fn as_query(&self) -> Option<&str> { self.uri().query() } - fn to_content_type(&self) -> Option { + fn to_content_type(&self) -> Option> { self.headers() .get(CONTENT_TYPE) - .map(|h| String::from_utf8_lossy(h.as_bytes()).to_string()) + .map(|h| String::from_utf8_lossy(h.as_bytes())) + } + + fn accepts(&self) -> Option> { + self.headers() + .get(ACCEPT) + .map(|h| String::from_utf8_lossy(h.as_bytes())) + } + + fn referer(&self) -> Option> { + self.headers() + .get(REFERER) + .map(|h| String::from_utf8_lossy(h.as_bytes())) } async fn try_into_bytes(self) -> Result> { diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index 871a15658..eee926a3b 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -1,7 +1,7 @@ use crate::error::ServerFnError; use bytes::Bytes; use futures::Stream; -use std::future::Future; +use std::{borrow::Cow, future::Future}; /// Request types for Actix. #[cfg(any(feature = "actix", doc))] @@ -73,7 +73,13 @@ where fn as_query(&self) -> Option<&str>; /// Returns the `Content-Type` header, if any. - fn to_content_type(&self) -> Option; + fn to_content_type(&self) -> Option>; + + /// Returns the `Accepts` header, if any. + fn accepts(&self) -> Option>; + + /// Returns the `Referer` header, if any. + fn referer(&self) -> Option>; /// Attempts to extract the body of the request into [`Bytes`]. fn try_into_bytes( @@ -103,10 +109,17 @@ impl Req for BrowserMockReq { unreachable!() } - fn to_content_type(&self) -> Option { + fn to_content_type(&self) -> Option> { unreachable!() } + fn accepts(&self) -> Option> { + unreachable!() + } + + fn referer(&self) -> Option> { + unreachable!() + } async fn try_into_bytes(self) -> Result> { unreachable!() } diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index 4f0ecb6cb..711268e71 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -1,10 +1,13 @@ use super::Res; use crate::error::{ - ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, ServerFnUrlError, - SERVER_FN_ERROR_HEADER, + ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER, }; use actix_web::{ - http::{header, StatusCode}, + http::{ + header, + header::{HeaderValue, LOCATION}, + StatusCode, + }, HttpResponse, }; use bytes::Bytes; @@ -77,11 +80,18 @@ where ))) } - fn error_response(path: &str, err: ServerFnError) -> Self { + fn error_response(path: &str, err: &ServerFnError) -> Self { ActixResponse(SendWrapper::new( HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR) .append_header((SERVER_FN_ERROR_HEADER, path)) .body(err.ser().unwrap_or_else(|_| err.to_string())), )) } + + fn redirect(&mut self, path: &str) { + if let Ok(path) = HeaderValue::from_str(path) { + *self.0.status_mut() = StatusCode::FOUND; + self.0.headers_mut().insert(LOCATION, path); + } + } } diff --git a/server_fn/src/response/http.rs b/server_fn/src/response/http.rs index ed6176bcd..fa28515c5 100644 --- a/server_fn/src/response/http.rs +++ b/server_fn/src/response/http.rs @@ -3,7 +3,7 @@ use crate::error::{ServerFnError, ServerFnErrorErr}; use axum::body::Body; use bytes::Bytes; use futures::{Stream, StreamExt}; -use http::Response; +use http::{header, HeaderValue, Response, StatusCode}; use std::fmt::{Debug, Display}; impl Res for Response @@ -50,10 +50,17 @@ where .map_err(|e| ServerFnError::Response(e.to_string())) } - fn error_response(path: &str, err: ServerFnError) -> Self { + fn error_response(path: &str, err: &ServerFnError) -> Self { Response::builder() .status(http::StatusCode::INTERNAL_SERVER_ERROR) .body(Body::from(err.to_string())) .unwrap() } + + fn redirect(&mut self, path: &str) { + if let Ok(path) = HeaderValue::from_str(path) { + self.headers_mut().insert(header::LOCATION, path); + *self.status_mut() = StatusCode::FOUND; + } + } } diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 620b5395f..6de74b1cc 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -42,7 +42,10 @@ where ) -> Result>; /// Converts an error into a response, with a `500` status code and the error text as its body. - fn error_response(path: &str, err: ServerFnError) -> Self; + fn error_response(path: &str, err: &ServerFnError) -> Self; + + /// Redirect the response by setting a 302 code and Location header. + fn redirect(&mut self, path: &str); } /// Represents the response as received by the client. @@ -101,7 +104,7 @@ impl Res for BrowserMockRes { unreachable!() } - fn error_response(_path: &str, _err: ServerFnError) -> Self { + fn error_response(_path: &str, _err: &ServerFnError) -> Self { unreachable!() } @@ -109,6 +112,10 @@ impl Res for BrowserMockRes { _content_type: &str, _data: impl Stream>>, ) -> Result> { - todo!() + unreachable!() + } + + fn redirect(&mut self, _path: &str) { + unreachable!() } }