Skip to main content

trillium_caching_headers/
etag.rs

1use crate::CachingHeadersExt;
2use etag::EntityTag;
3use trillium::{Conn, Handler, KnownHeaderName, Method, Status};
4
5/// # Etag and If-None-Match header handler
6///
7/// Trillium handler that provides an outbound [`etag
8/// header`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag)
9/// after other handlers have been run, and if the request includes an
10/// [`if-none-match`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match)
11/// header, compares these values and sends a
12/// [`304 not modified`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304) status,
13/// omitting the response body.
14///
15/// The conditional comparison applies only to `GET` and `HEAD` requests with successful
16/// responses; responses to other methods, error responses, and redirects pass through unchanged,
17/// and only successful responses receive a generated etag. Enforcing preconditions on
18/// state-changing requests — refusing the write with `412 Precondition Failed` — requires
19/// knowing the resource's entity tag before the write runs, so it is up to the application.
20///
21/// ## Streamed bodies
22///
23/// Note that this handler does not currently provide an etag trailer for
24/// streamed bodies, but may do so in the future.
25///
26/// ## Strong vs weak comparison
27///
28/// Etags can be compared using a strong method or a weak
29/// method. By default, this handler allows weak comparison. To change
30/// this setting, construct your handler with `Etag::new().strong()`.
31/// See [`etag::EntityTag`](https://docs.rs/etag/3.0.0/etag/struct.EntityTag.html#comparison)
32/// for further documentation.
33#[derive(Default, Clone, Copy, Debug)]
34pub struct Etag {
35    strong: bool,
36}
37
38impl Etag {
39    /// constructs a new Etag handler
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Configures this handler to use strong content-based etag
45    /// comparison only. See
46    /// [`etag::EntityTag`](https://docs.rs/etag/3.0.0/etag/struct.EntityTag.html#comparison)
47    /// for further documentation on the differences between strong
48    /// and weak etag comparison.
49    pub fn strong(mut self) -> Self {
50        self.strong = true;
51        self
52    }
53}
54
55impl Handler for Etag {
56    async fn run(&self, conn: Conn) -> Conn {
57        conn
58    }
59
60    async fn before_send(&self, mut conn: Conn) -> Conn {
61        // RFC 9110 §13.2.1: preconditions apply only when the response would otherwise be
62        // successful, and §13.1.2 answers a matching `If-None-Match` with `304 Not Modified`
63        // only for GET and HEAD. By `before_send` any other method has already run, so the
64        // only safe treatment of its response is to pass it through untouched.
65        let successful = conn.status().is_none_or(|status| status.is_success());
66        let preconditions_apply = successful && matches!(conn.method(), Method::Get | Method::Head);
67
68        // `If-None-Match: *` matches any current representation (a body).
69        if conn.request_headers().get_str(KnownHeaderName::IfNoneMatch) == Some("*") {
70            if preconditions_apply && conn.response_body().is_some() {
71                return conn.with_status(Status::NotModified);
72            }
73            return conn;
74        }
75
76        let if_none_match = conn.if_none_match();
77
78        let etag = conn.etag().or_else(|| {
79            // a generated entity tag on an error or redirect body would invite caches to
80            // revalidate against a representation that isn't the resource
81            if !successful {
82                return None;
83            }
84
85            let etag = conn
86                .response_body()
87                .and_then(|body| body.static_bytes())
88                .map(EntityTag::from_data);
89
90            if let Some(ref entity_tag) = etag {
91                conn.set_etag(entity_tag);
92            }
93
94            etag
95        });
96
97        if !preconditions_apply {
98            return conn;
99        }
100
101        if let (Some(ref etag), Some(ref if_none_match)) = (etag, if_none_match) {
102            let eq = if self.strong {
103                etag.strong_eq(if_none_match)
104            } else {
105                etag.weak_eq(if_none_match)
106            };
107
108            if eq {
109                return conn.with_status(Status::NotModified);
110            }
111        }
112
113        conn
114    }
115}