Skip to content

Commit

Permalink
generalize error redirect behavior across integrations
Browse files Browse the repository at this point in the history
  • Loading branch information
gbj committed Jan 14, 2024
1 parent d7755eb commit 50bec1c
Show file tree
Hide file tree
Showing 14 changed files with 159 additions and 157 deletions.
1 change: 0 additions & 1 deletion integrations/actix/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
86 changes: 2 additions & 84 deletions integrations/actix/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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)]
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion leptos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 1 addition & 4 deletions leptos_server/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion server_fn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
61 changes: 26 additions & 35 deletions server_fn/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -403,62 +404,52 @@ impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> {
}
}

/// 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<CustErr> {
path: String,
error: ServerFnError<CustErr>,
}

impl<CustErr> FromStr for ServerFnUrlError<CustErr>
where
CustErr: FromStr + Display,
{
type Err = ();

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once('|') {
None => Err(()),
Some((path, error)) => {
let error = ServerFnError::<CustErr>::de(error);
Ok(ServerFnUrlError {
path: path.to_string(),
error,
})
}
}
}
}

impl<CustErr> Display for ServerFnUrlError<CustErr>
where
CustErr: FromStr + Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}|", self.path)?;
write!(f, "{}", &self.error.ser()?)?;
Ok(())
}
}

impl<CustErr> ServerFnUrlError<CustErr> {
/// 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<CustErr>) -> Self {
Self {
path: path.to_string(),
error,
}
}

/// TODO: Write documentation
/// The error itself.
pub fn error(&self) -> &ServerFnError<CustErr> {
&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<Url, url::ParseError>
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<CustErr> From<ServerFnUrlError<CustErr>> for ServerFnError<CustErr> {
Expand Down
43 changes: 37 additions & 6 deletions server_fn/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#![forbid(unsafe_code)]
// uncomment this if you want to feel pain
#![deny(missing_docs)]

//! # Server Functions
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -236,10 +235,42 @@ where
fn run_on_server(
req: Self::ServerRequest,
) -> impl Future<Output = Self::ServerResponse> + 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 <form>. If they're accessed by a <form>, 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
}
}

Expand Down
6 changes: 3 additions & 3 deletions server_fn/src/middleware/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ mod axum {
Box::pin(async move {
inner.await.unwrap_or_else(|e| {
let err = ServerFnError::from(e);
Response::<Body>::error_response(&path, err)
Response::<Body>::error_response(&path, &err)
})
})
}
Expand Down Expand Up @@ -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()
})
})
}
Expand All @@ -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()
}))
})
}
Expand Down
26 changes: 19 additions & 7 deletions server_fn/src/request/actix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -17,6 +17,14 @@ impl ActixRequest {
pub fn take(self) -> (HttpRequest, Payload) {
self.0.take()
}

fn header(&self, name: &str) -> Option<Cow<'_, str>> {
self.0
.0
.headers()
.get(name)
.map(|h| String::from_utf8_lossy(h.as_bytes()))
}
}

impl From<(HttpRequest, Payload)> for ActixRequest {
Expand All @@ -30,12 +38,16 @@ impl<CustErr> Req<CustErr> for ActixRequest {
self.0 .0.uri().query()
}

fn to_content_type(&self) -> Option<String> {
self.0
.0
.headers()
.get("Content-Type")
.map(|h| String::from_utf8_lossy(h.as_bytes()).to_string())
fn to_content_type(&self) -> Option<Cow<'_, str>> {
self.header("Content-Type")
}

fn accepts(&self) -> Option<Cow<'_, str>> {
self.header("Accept")
}

fn referer(&self) -> Option<Cow<'_, str>> {
self.header("Referer")
}

fn try_into_bytes(
Expand Down
Loading

0 comments on commit 50bec1c

Please sign in to comment.