From ea28c12bb65d51347a63ab82d2a348d80007d407 Mon Sep 17 00:00:00 2001 From: imbolc Date: Tue, 30 Apr 2024 10:40:10 +0600 Subject: [PATCH 1/3] A typo --- src/guard.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/guard.rs b/src/guard.rs index 73044d8..79899e6 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -48,7 +48,7 @@ impl<'a, S> Layer for HxRequestGuardLayer<'a> { } } -/// Tower service that implementes redirecting to non-partial routes. +/// Tower service that implements redirecting to non-partial routes. #[derive(Debug, Clone)] pub struct HxRequestGuard<'a, S> { inner: S, From d0db3a404202d9b1b0f38d675e4abbf470dd7646 Mon Sep 17 00:00:00 2001 From: imbolc Date: Tue, 30 Apr 2024 10:53:00 +0600 Subject: [PATCH 2/3] Vary headers, closes #14 --- README.md | 19 +++++++ src/responders.rs | 2 + src/responders/vary.rs | 123 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 src/responders/vary.rs diff --git a/README.md b/README.md index a176207..ba911b6 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,25 @@ any of your responses. | `HX-Trigger-After-Settle` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` | | `HX-Trigger-After-Swap` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` | +Also, there are corresponding cache-related headers, which you may want to add to +`GET` responses, depending on the htmx headers. + +_For example, if your server renders the full HTML when the `HX-Request` header is +missing or `false`, and it renders a fragment of that HTML when `HX-Request: true`, +you need to add `Vary: HX-Request`. That causes the cache to be keyed based on a +composite of the response URL and the `HX-Request` request header - rather than +being based just on the response URL._ + +Refer to [caching htmx docs section](https://htmx.org/docs/#caching) for details. + +| Header | Responder | +|-------------------------|---------------------| +| `Vary: HX-Request` | `VaryHxRequest` | +| `Vary: HX-Target` | `VaryHxTarget` | +| `Vary: HX-Trigger` | `VaryHxTrigger` | +| `Vary: HX-Trigger-Name` | `VaryHxTriggerName` | + + ## Request Guards __Requires features `guards`.__ diff --git a/src/responders.rs b/src/responders.rs index fdc06e4..e7de8d2 100644 --- a/src/responders.rs +++ b/src/responders.rs @@ -11,6 +11,8 @@ mod location; pub use location::*; mod trigger; pub use trigger::*; +mod vary; +pub use vary::*; const HX_SWAP_INNER_HTML: &str = "innerHTML"; const HX_SWAP_OUTER_HTML: &str = "outerHTML"; diff --git a/src/responders/vary.rs b/src/responders/vary.rs new file mode 100644 index 0000000..1ee7312 --- /dev/null +++ b/src/responders/vary.rs @@ -0,0 +1,123 @@ +use axum_core::response::{IntoResponseParts, ResponseParts}; +use http::header::VARY; + +use crate::{extractors, headers, HxError}; + +/// The `Vary: HX-Request` header. +/// +/// You may want to add this header to the response if your handler responds differently based on +/// the `HX-Request` request header. +/// +/// For example, if your server renders the full HTML when the `HX-Request` header is missing or +/// `false`, and it renders a fragment of that HTML when `HX-Request: true`. +/// +/// You probably need this only for `GET` requests, as other HTTP methods are not cached by default. +/// +/// See for more information. +#[derive(Debug, Clone)] +pub struct VaryHxRequest; + +impl IntoResponseParts for VaryHxRequest { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut() + .insert(VARY, headers::HX_REQUEST.try_into()?); + + Ok(res) + } +} + +impl extractors::HxRequest { + /// Convenience method to create the corresponding `Vary` response header + pub fn vary_response() -> VaryHxRequest { + VaryHxRequest + } +} + +/// The `Vary: HX-Target` header. +/// +/// You may want to add this header to the response if your handler responds differently based on +/// the `HX-Target` request header. +/// +/// You probably need this only for `GET` requests, as other HTTP methods are not cached by default. +/// +/// See for more information. +#[derive(Debug, Clone)] +pub struct VaryHxTarget; + +impl IntoResponseParts for VaryHxTarget { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut() + .insert(VARY, headers::HX_TARGET.try_into()?); + + Ok(res) + } +} + +impl extractors::HxTarget { + /// Convenience method to create the corresponding `Vary` response header + pub fn vary_response() -> VaryHxTarget { + VaryHxTarget + } +} + +/// The `Vary: HX-Trigger` header. +/// +/// You may want to add this header to the response if your handler responds differently based on +/// the `HX-Trigger` request header. +/// +/// You probably need this only for `GET` requests, as other HTTP methods are not cached by default. +/// +/// See for more information. +#[derive(Debug, Clone)] +pub struct VaryHxTrigger; + +impl IntoResponseParts for VaryHxTrigger { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut() + .insert(VARY, headers::HX_TRIGGER.try_into()?); + + Ok(res) + } +} + +impl extractors::HxTrigger { + /// Convenience method to create the corresponding `Vary` response header + pub fn vary_response() -> VaryHxTrigger { + VaryHxTrigger + } +} + +/// The `Vary: HX-Trigger-Name` header. +/// +/// You may want to add this header to the response if your handler responds differently based on +/// the `HX-Trigger-Name` request header. +/// +/// You probably need this only for `GET` requests, as other HTTP methods are not cached by default. +/// +/// See for more information. +#[derive(Debug, Clone)] +pub struct VaryHxTriggerName; + +impl IntoResponseParts for VaryHxTriggerName { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut() + .insert(VARY, headers::HX_TRIGGER_NAME.try_into()?); + + Ok(res) + } +} + +impl extractors::HxTriggerName { + /// Convenience method to create the corresponding `Vary` response header + pub fn vary_response() -> VaryHxTriggerName { + VaryHxTriggerName + } +} From 59a07a1e4a8a32257765868ba871b53ac623100c Mon Sep 17 00:00:00 2001 From: imbolc Date: Mon, 6 May 2024 10:41:03 +0600 Subject: [PATCH 3/3] Allow multiple vary headers --- Cargo.toml | 3 +++ README.md | 6 ++++++ src/error.rs | 13 ++++++++++++- src/responders/vary.rs | 36 +++++++++++++++++++++++++++--------- 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d3c3655..8866016 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,9 @@ serde_json = { version = "1", optional = true } [dev-dependencies] axum = { version = "0.7", default-features = false } +axum-test = "14" +tokio = { version = "1", features = ["full"] } +tokio-test = "0.4" [package.metadata.docs.rs] all-features = true diff --git a/README.md b/README.md index ba911b6..a7a3ee3 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,12 @@ Contributions are always welcome! If you have an idea for a feature or find a bug, let me know. PR's are appreciated, but if it's not a small change, please open an issue first so we're all on the same page! +### Testing + +```sh +cargo +nightly test --all-features +``` + ## License `axum-htmx` is dual-licensed under either diff --git a/src/error.rs b/src/error.rs index d89d096..1c2f782 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,11 +1,15 @@ use std::{error, fmt}; use axum_core::response::IntoResponse; -use http::{header::InvalidHeaderValue, StatusCode}; +use http::{ + header::{InvalidHeaderValue, MaxSizeReached}, + StatusCode, +}; #[derive(Debug)] pub enum HxError { InvalidHeaderValue(InvalidHeaderValue), + TooManyResponseHeaders(MaxSizeReached), #[cfg(feature = "serde")] #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))] @@ -18,6 +22,12 @@ impl From for HxError { } } +impl From for HxError { + fn from(value: MaxSizeReached) -> Self { + Self::TooManyResponseHeaders(value) + } +} + #[cfg(feature = "serde")] #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))] impl From for HxError { @@ -30,6 +40,7 @@ impl fmt::Display for HxError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { HxError::InvalidHeaderValue(err) => write!(f, "Invalid header value: {err}"), + HxError::TooManyResponseHeaders(err) => write!(f, "Too many response headers: {err}"), #[cfg(feature = "serde")] HxError::Json(err) => write!(f, "Json: {err}"), } diff --git a/src/responders/vary.rs b/src/responders/vary.rs index 1ee7312..03866da 100644 --- a/src/responders/vary.rs +++ b/src/responders/vary.rs @@ -1,8 +1,13 @@ use axum_core::response::{IntoResponseParts, ResponseParts}; -use http::header::VARY; +use http::header::{HeaderValue, VARY}; use crate::{extractors, headers, HxError}; +const HX_REQUEST: HeaderValue = HeaderValue::from_static(headers::HX_REQUEST); +const HX_TARGET: HeaderValue = HeaderValue::from_static(headers::HX_TARGET); +const HX_TRIGGER: HeaderValue = HeaderValue::from_static(headers::HX_TRIGGER); +const HX_TRIGGER_NAME: HeaderValue = HeaderValue::from_static(headers::HX_TRIGGER_NAME); + /// The `Vary: HX-Request` header. /// /// You may want to add this header to the response if your handler responds differently based on @@ -21,8 +26,7 @@ impl IntoResponseParts for VaryHxRequest { type Error = HxError; fn into_response_parts(self, mut res: ResponseParts) -> Result { - res.headers_mut() - .insert(VARY, headers::HX_REQUEST.try_into()?); + res.headers_mut().try_append(VARY, HX_REQUEST)?; Ok(res) } @@ -50,8 +54,7 @@ impl IntoResponseParts for VaryHxTarget { type Error = HxError; fn into_response_parts(self, mut res: ResponseParts) -> Result { - res.headers_mut() - .insert(VARY, headers::HX_TARGET.try_into()?); + res.headers_mut().try_append(VARY, HX_TARGET)?; Ok(res) } @@ -79,8 +82,7 @@ impl IntoResponseParts for VaryHxTrigger { type Error = HxError; fn into_response_parts(self, mut res: ResponseParts) -> Result { - res.headers_mut() - .insert(VARY, headers::HX_TRIGGER.try_into()?); + res.headers_mut().try_append(VARY, HX_TRIGGER)?; Ok(res) } @@ -108,8 +110,7 @@ impl IntoResponseParts for VaryHxTriggerName { type Error = HxError; fn into_response_parts(self, mut res: ResponseParts) -> Result { - res.headers_mut() - .insert(VARY, headers::HX_TRIGGER_NAME.try_into()?); + res.headers_mut().try_append(VARY, HX_TRIGGER_NAME)?; Ok(res) } @@ -121,3 +122,20 @@ impl extractors::HxTriggerName { VaryHxTriggerName } } + +#[cfg(test)] +mod tests { + use super::*; + use axum::{routing::get, Router}; + use std::collections::hash_set::HashSet; + + #[tokio::test] + async fn multiple_headers() { + let app = Router::new().route("/", get(|| async { (VaryHxRequest, VaryHxTarget, "foo") })); + let server = axum_test::TestServer::new(app).unwrap(); + + let resp = server.get("/").await; + let values: HashSet = resp.iter_headers_by_name("vary").cloned().collect(); + assert_eq!(values, HashSet::from([HX_REQUEST, HX_TARGET])); + } +}