static_web_server/
control_headers.rs1use hyper::{
11 Request, Response,
12 header::{CACHE_CONTROL, HeaderValue},
13};
14
15use crate::body::Body;
16use crate::{Error, handler::RequestHandlerOpts};
17
18static CACHE_CONTROL_DEFAULT: HeaderValue = HeaderValue::from_static("no-cache");
20static CACHE_CONTROL_ONE_HOUR: HeaderValue = HeaderValue::from_static("max-age=3600");
21static CACHE_CONTROL_ONE_YEAR: HeaderValue = HeaderValue::from_static("max-age=31536000");
22
23const CACHE_EXT_ONE_HOUR: [&str; 2] = ["atom", "rss"];
25const CACHE_EXT_ONE_YEAR: [&str; 32] = [
26 "avif", "bmp", "bz2", "css", "doc", "gif", "gz", "htc", "ico", "jpeg", "jpg", "js", "jxl",
27 "map", "mjs", "mp3", "mp4", "ogg", "ogv", "pdf", "png", "rar", "rtf", "tar", "tgz", "wav",
28 "weba", "webm", "webp", "woff", "woff2", "zip",
29];
30
31pub(crate) fn init(enabled: bool, handler_opts: &mut RequestHandlerOpts) {
32 handler_opts.cache_control_headers = enabled;
33 tracing::info!(enabled, "cache control headers");
34}
35
36pub(crate) fn post_process<T>(
38 opts: &RequestHandlerOpts,
39 req: &Request<T>,
40 mut resp: Response<Body>,
41) -> Result<Response<Body>, Error> {
42 if opts.cache_control_headers {
43 append_headers(req.uri().path(), &mut resp);
44 }
45 Ok(resp)
46}
47
48pub fn append_headers(uri: &str, resp: &mut Response<Body>) {
50 let header_value = get_cache_control_header(uri);
51 resp.headers_mut()
52 .insert(CACHE_CONTROL, header_value.clone());
53}
54
55#[inline(always)]
59fn get_file_extension(uri: &str) -> Option<&str> {
60 uri.rsplit_once('.').map(|(_, rest)| rest)
61}
62
63#[inline(always)]
65fn get_cache_control_header(uri: &str) -> &'static HeaderValue {
66 if let Some(extension) = get_file_extension(uri) {
67 let mut buf = [0u8; 16];
69 if extension.len() <= buf.len() {
70 let ext_bytes = &mut buf[..extension.len()];
71 ext_bytes.copy_from_slice(extension.as_bytes());
72 ext_bytes.make_ascii_lowercase();
73
74 if let Ok(ext_lower) = std::str::from_utf8(ext_bytes) {
75 if CACHE_EXT_ONE_HOUR.binary_search(&ext_lower).is_ok() {
76 return &CACHE_CONTROL_ONE_HOUR;
77 } else if CACHE_EXT_ONE_YEAR.binary_search(&ext_lower).is_ok() {
78 return &CACHE_CONTROL_ONE_YEAR;
79 }
80 }
81 } else {
82 let ext_lower = extension.to_ascii_lowercase();
84 if CACHE_EXT_ONE_HOUR
85 .binary_search(&ext_lower.as_str())
86 .is_ok()
87 {
88 return &CACHE_CONTROL_ONE_HOUR;
89 } else if CACHE_EXT_ONE_YEAR
90 .binary_search(&ext_lower.as_str())
91 .is_ok()
92 {
93 return &CACHE_CONTROL_ONE_YEAR;
94 }
95 }
96 }
97 &CACHE_CONTROL_DEFAULT
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use hyper::{Response, StatusCode};
104
105 #[test]
106 fn test_arrays_are_sorted() {
107 assert!(
108 CACHE_EXT_ONE_HOUR.windows(2).all(|w| w[0] < w[1]),
109 "CACHE_EXT_ONE_HOUR is not sorted!"
110 );
111 assert!(
112 CACHE_EXT_ONE_YEAR.windows(2).all(|w| w[0] < w[1]),
113 "CACHE_EXT_ONE_YEAR is not sorted!"
114 );
115 }
116
117 #[test]
118 fn headers_case_insensitivity() {
119 let mut resp = Response::new(crate::body::empty());
120 append_headers("/assets/script.JS", &mut resp);
121 let cache_control = resp.headers().get(CACHE_CONTROL).unwrap();
122 assert_eq!(cache_control.to_str().unwrap(), "max-age=31536000");
123
124 append_headers("/assets/IMAGE.PNG", &mut resp);
125 let cache_control = resp.headers().get(CACHE_CONTROL).unwrap();
126 assert_eq!(cache_control.to_str().unwrap(), "max-age=31536000");
127 }
128
129 #[test]
130 fn headers_one_hour() {
131 let mut resp = Response::new(crate::body::empty());
132 *resp.status_mut() = StatusCode::OK;
133
134 for ext in CACHE_EXT_ONE_HOUR.iter() {
135 append_headers(&["/some.", ext].concat(), &mut resp);
136 let cache_control = resp.headers().get(CACHE_CONTROL).unwrap();
137 assert_eq!(cache_control.to_str().unwrap(), "max-age=3600");
138 }
139 }
140
141 #[test]
142 fn headers_default_fallback() {
143 let mut resp = Response::new(crate::body::empty());
144 *resp.status_mut() = StatusCode::OK;
145
146 append_headers("/", &mut resp);
147 assert_eq!(
148 resp.headers().get(CACHE_CONTROL).unwrap().to_str().unwrap(),
149 "no-cache"
150 );
151
152 append_headers("/index.html", &mut resp);
153 assert_eq!(
154 resp.headers().get(CACHE_CONTROL).unwrap().to_str().unwrap(),
155 "no-cache"
156 );
157
158 append_headers("/config.json", &mut resp);
159 assert_eq!(
160 resp.headers().get(CACHE_CONTROL).unwrap().to_str().unwrap(),
161 "no-cache"
162 );
163
164 append_headers("/api/data", &mut resp);
165 assert_eq!(
166 resp.headers().get(CACHE_CONTROL).unwrap().to_str().unwrap(),
167 "no-cache"
168 );
169 }
170
171 #[test]
172 fn headers_one_year() {
173 let mut resp = Response::new(crate::body::empty());
174 *resp.status_mut() = StatusCode::OK;
175
176 for ext in CACHE_EXT_ONE_YEAR.iter() {
177 append_headers(&["/some.", ext].concat(), &mut resp);
178 let cache_control = resp.headers().get(CACHE_CONTROL).unwrap();
179 assert_eq!(cache_control.to_str().unwrap(), "max-age=31536000");
180 }
181 }
182}