diff --git a/Cargo.toml b/Cargo.toml index 36dea9d..01fb0eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ default = [] unstable = [] guards = ["tower", "futures-core", "pin-project-lite"] serde = ["dep:serde", "dep:serde_json"] +auto-vary = ["futures", "tokio", "tower"] [dependencies] axum-core = "0.4" @@ -30,9 +31,13 @@ pin-project-lite = { version = "0.2", optional = true } serde = { version = "1", features = ["derive"], optional = true } serde_json = { version = "1", optional = true } +# Optional dependencies required for the `auto-vary` feature. +tokio = { version = "1", features = ["sync"], optional = true } +futures = { version = "0.3", default-features = false, optional = true } + [dev-dependencies] axum = { version = "0.7", default-features = false } -axum-test = "14" +axum-test = "15" tokio = { version = "1", features = ["full"] } tokio-test = "0.4" diff --git a/README.md b/README.md index a7a3ee3..bee8226 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ - [Getting Started](#getting-started) - [Extractors](#extractors) - [Responders](#responders) + - [Auto Caching Management](#auto-caching-management) - [Request Guards](#request-guards) - [Examples](#examples) - [Example: Extractors](#example-extractors) @@ -76,6 +77,8 @@ any of your responses. | `HX-Trigger-After-Settle` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` | | `HX-Trigger-After-Swap` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` | +### Vary Responders + Also, there are corresponding cache-related headers, which you may want to add to `GET` responses, depending on the htmx headers. @@ -85,7 +88,7 @@ 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. +Refer to [caching htmx docs section][htmx-caching] for details. | Header | Responder | |-------------------------|---------------------| @@ -94,10 +97,27 @@ Refer to [caching htmx docs section](https://htmx.org/docs/#caching) for details | `Vary: HX-Trigger` | `VaryHxTrigger` | | `Vary: HX-Trigger-Name` | `VaryHxTriggerName` | +Look at the [Auto Caching Management](#auto-caching-management) section for +automatic `Vary` headers management. + +## Auto Caching Management + +__Requires feature `auto-vary`.__ + +Manual use of [Vary Reponders](#vary-responders) adds fragility to the code, +because of the need to manually control correspondence between used extractors +and the responders. + +We provide a [middleware](crate::AutoVaryLayer) to address this issue by +automatically adding `Vary` headers when corresponding extractors are used. +For example, on extracting [`HxRequest`], the middleware automatically adds +`Vary: hx-request` header to the response. + +Look at the usage [example][auto-vary-example]. ## Request Guards -__Requires features `guards`.__ +__Requires feature `guards`.__ In addition to the extractors, there is also a route-wide layer request guard for the `HX-Request` header. This will redirect any requests without the header @@ -207,10 +227,11 @@ fn router_two() -> Router { ## Feature Flags -| Flag | Default | Description | Dependencies | -|----------|----------|------------------------------------------------------------|---------------------------------------------| -| `guards` | Disabled | Adds request guard layers. | `tower`, `futures-core`, `pin-project-lite` | -| `serde` | Disabled | Adds serde support for the `HxEvent` and `LocationOptions` | `serde`, `serde_json` | +| Flag | Default | Description | Dependencies | +|-------------|----------|------------------------------------------------------------|---------------------------------------------| +| `auto-vary` | Disabled | A middleware to address [HTMx caching issue][htmx-caching] | `futures`, `tokio`, `tower` | +| `guards` | Disabled | Adds request guard layers. | `tower`, `futures-core`, `pin-project-lite` | +| `serde` | Disabled | Adds serde support for the `HxEvent` and `LocationOptions` | `serde`, `serde_json` | ## Contributing @@ -233,3 +254,6 @@ cargo +nightly test --all-features - **[Apache License, Version 2.0](/LICENSE-APACHE)** at your option. + +[htmx-caching]: https://htmx.org/docs/#caching +[auto-vary-example]: https://github.com/robertwayne/axum-htmx/blob/main/examples/auto-vary.rs diff --git a/examples/auto-vary.rs b/examples/auto-vary.rs new file mode 100644 index 0000000..b7868d5 --- /dev/null +++ b/examples/auto-vary.rs @@ -0,0 +1,40 @@ +//! Using `auto-vary` middleware +//! +//! Don't forget about the feature while running it: +//! `cargo run --features auto-vary --example auto-vary` +use std::time::Duration; + +use axum::{response::Html, routing::get, serve, Router}; +use axum_htmx::{AutoVaryLayer, HxRequest}; +use tokio::{net::TcpListener, time::sleep}; + +#[tokio::main] +async fn main() { + let app = Router::new() + .route("/", get(handler)) + // Add the middleware + .layer(AutoVaryLayer); + + let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap(); + serve(listener, app).await.unwrap(); +} + +// Our handler differentiates full-page GET requests from HTMx-based ones by looking at the `hx-request` +// requestheader. +// +// The middleware sees the usage of the `HxRequest` extractor and automatically adds the +// `Vary: hx-request` response header. +async fn handler(HxRequest(hx_request): HxRequest) -> Html<&'static str> { + if hx_request { + // For HTMx-based GET request, it returns a partial page update + sleep(Duration::from_secs(3)).await; + return Html("HTMx response"); + } + // While for a normal GET request, it returns the whole page + Html( + r#" + +
Loading ...
+ "#, + ) +} diff --git a/src/auto_vary.rs b/src/auto_vary.rs new file mode 100644 index 0000000..c8c0729 --- /dev/null +++ b/src/auto_vary.rs @@ -0,0 +1,228 @@ +//! A middleware to automatically add a `Vary` header when needed to address +//! [HTMx caching issue](https://htmx.org/docs/#caching) + +use std::{ + sync::Arc, + task::{Context, Poll}, +}; + +use axum_core::{ + extract::Request, + response::{IntoResponse, Response}, +}; +use futures::future::{join_all, BoxFuture}; +use http::{ + header::{HeaderValue, VARY}, + Extensions, +}; +use tokio::sync::oneshot::{self, Receiver, Sender}; +use tower::{Layer, Service}; + +use crate::{ + headers::{HX_REQUEST_STR, HX_TARGET_STR, HX_TRIGGER_NAME_STR, HX_TRIGGER_STR}, + HxError, +}; +#[cfg(doc)] +use crate::{HxRequest, HxTarget, HxTrigger, HxTriggerName}; + +const MIDDLEWARE_DOUBLE_USE: &str = + "Configuration error: `axum_httpx::vary_middleware` is used twice"; + +/// Addresses [HTMx caching issue](https://htmx.org/docs/#caching) +/// by automatically adding a corresponding `Vary` header when [`HxRequest`], [`HxTarget`], +/// [`HxTrigger`], [`HxTriggerName`] or their combination is used. +#[derive(Clone)] +pub struct AutoVaryLayer; + +/// Tower service for [`AutoVaryLayer`] +#[derive(Clone)] +pub struct AutoVaryMiddleware