trillium_caching_headers/modified.rs
1use crate::CachingHeadersExt;
2use trillium::{Conn, Handler, KnownHeaderName, Status};
3
4/// # A handler for the `Last-Modified` and `If-Modified-Since` header interaction.
5///
6/// This handler does not set a `Last-Modified` header on its own, but
7/// relies on other handlers doing so.
8///
9/// ## Precedence: `If-None-Match` wins
10///
11/// `If-Modified-Since` is evaluated only when the request carries no
12/// `If-None-Match`. Per [RFC 9110 §13.1.3][rfc]:
13///
14/// > A recipient MUST ignore If-Modified-Since if the request contains an
15/// > If-None-Match header field; the condition in If-None-Match is considered to
16/// > be a more accurate replacement for the condition in If-Modified-Since, and
17/// > the two are only combined for the sake of interoperating with older
18/// > intermediaries that might not implement If-None-Match.
19///
20/// This matters most in the case it is easiest to overlook: when the entity tag
21/// did *not* match. Honoring both conditions would let a coarse timestamp
22/// comparison override an [`Etag`] that had already determined the representation
23/// changed, answering `304` for a body that is genuinely new. Browsers routinely
24/// replay both headers, so any handler whose `Last-Modified` is coarser than its
25/// entity tag — a rendered response whose inputs are versioned rather than
26/// timestamped, say — would otherwise serve stale content.
27///
28/// [rfc]: https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3
29/// [`Etag`]: crate::Etag
30#[derive(Debug, Clone, Copy, Default)]
31pub struct Modified {
32 _private: (),
33}
34
35impl Modified {
36 /// constructs a new Modified handler
37 pub fn new() -> Self {
38 Self { _private: () }
39 }
40}
41
42impl Handler for Modified {
43 async fn before_send(&self, conn: Conn) -> Conn {
44 // RFC 9110 §13.1.3: an If-None-Match in the request wholly replaces
45 // If-Modified-Since — including when it did not match, which is exactly
46 // the case where evaluating both would go wrong.
47 //
48 // Presence of the header field, deliberately, not a successfully parsed
49 // entity tag: `*` and malformed tags do not parse (`if_none_match()`
50 // yields `None` for both), but the field is still there, and the spec
51 // conditions on the field. Falling back to a timestamp comparison
52 // because we could not read the tag would resurrect the very bug this
53 // guards against.
54 if conn
55 .request_headers()
56 .has_header(KnownHeaderName::IfNoneMatch)
57 {
58 return conn;
59 }
60
61 match (conn.if_modified_since(), conn.last_modified()) {
62 (Some(if_modified_since), Some(last_modified))
63 if last_modified <= if_modified_since =>
64 {
65 conn.with_status(Status::NotModified)
66 }
67
68 _ => conn,
69 }
70 }
71
72 async fn run(&self, conn: Conn) -> Conn {
73 conn
74 }
75}