trillium_caching_headers/etag.rs
1use crate::CachingHeadersExt;
2use etag::EntityTag;
3use trillium::{Conn, Handler, KnownHeaderName, 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/// ## Streamed bodies
16///
17/// Note that this handler does not currently provide an etag trailer for
18/// streamed bodies, but may do so in the future.
19///
20/// ## Strong vs weak comparison
21///
22/// Etags can be compared using a strong method or a weak
23/// method. By default, this handler allows weak comparison. To change
24/// this setting, construct your handler with `Etag::new().strong()`.
25/// See [`etag::EntityTag`](https://docs.rs/etag/3.0.0/etag/struct.EntityTag.html#comparison)
26/// for further documentation.
27#[derive(Default, Clone, Copy, Debug)]
28pub struct Etag {
29 strong: bool,
30}
31
32impl Etag {
33 /// constructs a new Etag handler
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 /// Configures this handler to use strong content-based etag
39 /// comparison only. See
40 /// [`etag::EntityTag`](https://docs.rs/etag/3.0.0/etag/struct.EntityTag.html#comparison)
41 /// for further documentation on the differences between strong
42 /// and weak etag comparison.
43 pub fn strong(mut self) -> Self {
44 self.strong = true;
45 self
46 }
47}
48
49impl Handler for Etag {
50 async fn run(&self, conn: Conn) -> Conn {
51 conn
52 }
53
54 async fn before_send(&self, mut conn: Conn) -> Conn {
55 // RFC 9110 ยง13.1.2: `If-None-Match: *` matches any current representation. When the
56 // response carries one (a successful status with a body), the precondition fails and
57 // we respond `304 Not Modified`.
58 if conn.request_headers().get_str(KnownHeaderName::IfNoneMatch) == Some("*") {
59 if conn.response_body().is_some() && conn.status().is_none_or(|s| s.is_success()) {
60 return conn.with_status(Status::NotModified);
61 }
62 return conn;
63 }
64
65 let if_none_match = conn.if_none_match();
66
67 let etag = conn.etag().or_else(|| {
68 let etag = conn
69 .response_body()
70 .and_then(|body| body.static_bytes())
71 .map(EntityTag::from_data);
72
73 if let Some(ref entity_tag) = etag {
74 conn.set_etag(entity_tag);
75 }
76
77 etag
78 });
79
80 if let (Some(ref etag), Some(ref if_none_match)) = (etag, if_none_match) {
81 let eq = if self.strong {
82 etag.strong_eq(if_none_match)
83 } else {
84 etag.weak_eq(if_none_match)
85 };
86
87 if eq {
88 return conn.with_status(Status::NotModified);
89 }
90 }
91
92 conn
93 }
94}