Skip to main content

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}