feature-gate the form redirect stuff, and clear old errors from query

This commit is contained in:
Greg Johnston 2024-01-14 20:19:47 -05:00
parent 90ba3529e9
commit 06c478b7cb
4 changed files with 39 additions and 6 deletions

View file

@ -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", "url", "cbor"] }
server_fn = { workspace = true, features = ["form-redirects", "browser", "url", "cbor"] }
web-sys = { version = "0.3.63", features = [
"ShadowRoot",
"ShadowRootInit",

View file

@ -68,6 +68,7 @@ url = "2"
[features]
default = [ "json", "cbor"]
form-redirects = []
actix = ["ssr", "dep:actix-web", "dep:send_wrapper"]
axum = [
"ssr",

View file

@ -408,7 +408,7 @@ impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> {
/// 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.
/// without JavaScript/WASM supported, by encoding it in the URL as a query string.
/// This is useful for progressive enhancement.
#[derive(Debug)]
pub struct ServerFnUrlError<CustErr> {
@ -450,6 +450,29 @@ impl<CustErr> ServerFnUrlError<CustErr> {
);
Ok(url)
}
/// Replaces any ServerFnUrlError info from the URL in the given string
/// with the serialized success value given.
pub fn strip_error_info(path: &mut String) {
if let Ok(mut url) = Url::parse(&*path) {
// NOTE: This is gross, but the Serializer you get from
// .query_pairs_mut() isn't an Iterator so you can't just .retain().
let pairs_previously = url
.query_pairs()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<Vec<_>>();
let mut pairs = url.query_pairs_mut();
pairs.clear();
for (key, value) in pairs_previously
.into_iter()
.filter(|(key, _)| key != "__path" && key != "__err")
{
pairs.append_pair(&key, &value);
}
drop(pairs);
*path = url.to_string();
}
}
}
impl<CustErr> From<ServerFnUrlError<CustErr>> for ServerFnError<CustErr> {

View file

@ -238,10 +238,13 @@ where
// 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 accepts_html = if cfg!(feature = "form-redirects") {
req.accepts()
.map(|n| n.contains("text/html"))
.unwrap_or(false)
} else {
false
};
let mut referer = req.referer().as_deref().map(ToOwned::to_owned);
async move {
@ -256,6 +259,7 @@ where
});
// if it accepts HTML, we'll redirect to the Referer
#[cfg(feature = "form-redirects")]
if accepts_html {
// if it had an error, encode that error in the URL
if let Some(err) = err {
@ -265,6 +269,11 @@ where
referer = Some(url.to_string());
}
}
// otherwise, strip error info from referer URL, as that means it's from a previous
// call
else if let Some(referer) = referer.as_mut() {
ServerFnUrlError::<Self::Error>::strip_error_info(referer)
}
// set the status code and Location header
res.redirect(referer.as_deref().unwrap_or("/"));