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