Skip to main content

rocket_cache_response/
lib.rs

1/*!
2# Cache Response for Rocket Framework
3
4This crate provides a response struct used for HTTP cache control.
5
6```rust
7use rocket::get;
8use rocket_cache_response::CacheResponse;
9
10#[get("/")]
11fn index() -> CacheResponse<&'static str> {
12    CacheResponse::public("Hello world!", 3600)
13}
14```
15
16Every directive is a public field of `CacheControl`, so an uncommon combination can be built with the struct update syntax.
17
18```rust
19use rocket::get;
20use rocket_cache_response::{CacheControl, CacheResponse};
21
22#[get("/")]
23fn index() -> CacheResponse<&'static str> {
24    CacheResponse::new("Hello world!", CacheControl {
25        s_max_age:              Some(86400),
26        stale_while_revalidate: Some(30),
27        ..CacheControl::public(60)
28    })
29}
30```
31
32A browser holding on to an old response is a nuisance during development, so `only_release` drops the `Cache-Control` header when the program is built in the debug mode.
33
34```rust
35use rocket::get;
36use rocket_cache_response::CacheResponse;
37
38#[get("/")]
39fn index() -> CacheResponse<&'static str> {
40    CacheResponse::public("Hello world!", 3600).only_release()
41}
42```
43
44## Which `Cache-Control` Should I Use?
45
46| Situation | Directives | Shortcut |
47| --------- | ---------- | -------- |
48| A static file whose URL changes whenever its content changes, such as `app.9f2c1a.js` | `public, max-age=31536000, immutable` | `CacheResponse::immutable(responder)` |
49| A public page or asset that only changes once in a while | `public, max-age=3600` | `CacheResponse::public(responder, 3600)` |
50| A public response that a CDN should keep longer than a browser does | `public, max-age=60, s-maxage=86400` | `CacheControl { s_max_age: Some(86400), ..CacheControl::public(60) }` |
51| A page or an API response that belongs to the logged-in user | `private, max-age=0` | `CacheResponse::private(responder, 0)` |
52| A response that has to be checked with the server before every reuse | `no-cache` | `CacheResponse::no_cache(responder)` |
53| Personal data, payment details or anything else sensitive | `no-store` | `CacheResponse::no_store(responder)` |
54
55Things that are easy to get wrong:
56
57* `no-cache` does not mean "do not cache". A cache may still store the response; it just has to ask the origin server whether the stored copy is still good before every reuse. Use `no-store` when the response must never be written to a cache at all.
58* `no-cache` on its own still lets shared caches, such as a CDN or a company proxy, store the response. Add `private` whenever the body is meant for one user only.
59* `must-revalidate` only takes effect after `max-age` has passed. It forbids a cache from serving the stale copy while the origin server is unreachable.
60* `immutable` is honored even when the user presses the reload button, so only use it for URLs that get a new name whenever the content changes.
61* `max-age` is counted by each cache on its own, so a CDN and a browser may hold their copies for different amounts of time. `s-maxage` is the way to give shared caches a lifetime of their own.
62*/
63
64pub extern crate rocket;
65
66mod cache_control;
67
68use rocket::{
69    request::Request,
70    response::{Responder, Response, Result},
71};
72
73pub use crate::cache_control::{CacheControl, Cacheability};
74
75/// The responder with a `Cache-Control` header.
76#[derive(Debug, Clone)]
77pub struct CacheResponse<R> {
78    /// The responder that produces the response itself.
79    pub responder:     R,
80    /// The directives to send. `None` adds no `Cache-Control` header at all.
81    pub cache_control: Option<CacheControl>,
82}
83
84impl<R> CacheResponse<R> {
85    /// Attach the given directives to a responder. Passing `None` adds no `Cache-Control` header at all.
86    #[inline]
87    pub fn new(responder: R, cache_control: impl Into<Option<CacheControl>>) -> Self {
88        Self {
89            responder,
90            cache_control: cache_control.into(),
91        }
92    }
93
94    /// `public, max-age=<max_age>`: every cache, shared ones included, may reuse the response for `max_age` seconds.
95    #[inline]
96    pub fn public(responder: R, max_age: u32) -> Self {
97        Self::new(responder, CacheControl::public(max_age))
98    }
99
100    /// `private, max-age=<max_age>`: only the browser that made the request may reuse the response, for `max_age` seconds.
101    #[inline]
102    pub fn private(responder: R, max_age: u32) -> Self {
103        Self::new(responder, CacheControl::private(max_age))
104    }
105
106    /// `no-cache`: a cache may store the response, but it must ask the origin server whether the stored copy is still good before every reuse.
107    #[inline]
108    pub fn no_cache(responder: R) -> Self {
109        Self::new(responder, CacheControl::no_cache())
110    }
111
112    /// `no-store`: no cache may store the response at all.
113    #[inline]
114    pub fn no_store(responder: R) -> Self {
115        Self::new(responder, CacheControl::no_store())
116    }
117
118    /// `public, max-age=31536000, immutable`: the response never changes, so it should be reused for a year without any revalidation.
119    #[inline]
120    pub fn immutable(responder: R) -> Self {
121        Self::new(responder, CacheControl::immutable())
122    }
123
124    /// Drop the `Cache-Control` header when this program is built in the **debug** mode, so a browser always fetches the newest response during development.
125    #[inline]
126    pub fn only_release(mut self) -> Self {
127        if cfg!(debug_assertions) {
128            self.cache_control = None;
129        }
130
131        self
132    }
133}
134
135impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for CacheResponse<R> {
136    fn respond_to(self, request: &'r Request<'_>) -> Result<'o> {
137        let response = self.responder.respond_to(request)?;
138
139        let header_value = match self.cache_control {
140            Some(cache_control) => cache_control.to_header_value(),
141            None => return Ok(response),
142        };
143
144        if header_value.is_empty() {
145            // `CacheControl::new()` sets no directive, and an empty header value is worse than no header.
146            return Ok(response);
147        }
148
149        Response::build_from(response).raw_header("Cache-Control", header_value).ok()
150    }
151}