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}