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}