static_web_server/
control_headers.rs1use hyper::{Body, Request, Response};
11
12use crate::{handler::RequestHandlerOpts, Error};
13
14const MAX_AGE_ONE_HOUR: u64 = 60 * 60;
16const MAX_AGE_ONE_DAY: u64 = 60 * 60 * 24;
17const MAX_AGE_ONE_YEAR: u64 = 60 * 60 * 24 * 365;
18
19const CACHE_EXT_ONE_HOUR: [&str; 4] = ["atom", "json", "rss", "xml"];
21const CACHE_EXT_ONE_YEAR: [&str; 32] = [
22 "avif", "bmp", "bz2", "css", "doc", "gif", "gz", "htc", "ico", "jpeg", "jpg", "js", "jxl",
23 "map", "mjs", "mp3", "mp4", "ogg", "ogv", "pdf", "png", "rar", "rtf", "tar", "tgz", "wav",
24 "weba", "webm", "webp", "woff", "woff2", "zip",
25];
26
27pub(crate) fn init(enabled: bool, handler_opts: &mut RequestHandlerOpts) {
28 handler_opts.cache_control_headers = enabled;
29 server_info!("cache control headers: enabled={enabled}");
30}
31
32pub(crate) fn post_process<T>(
34 opts: &RequestHandlerOpts,
35 req: &Request<T>,
36 mut resp: Response<Body>,
37) -> Result<Response<Body>, Error> {
38 if opts.cache_control_headers {
39 append_headers(req.uri().path(), &mut resp);
40 }
41 Ok(resp)
42}
43
44pub fn append_headers(uri: &str, resp: &mut Response<Body>) {
46 let max_age = get_max_age(uri);
47 resp.headers_mut().insert(
48 "cache-control",
49 format!(
50 "public, max-age={}",
51 std::cmp::min(max_age, u32::MAX as u64)
53 )
54 .parse()
55 .unwrap(),
56 );
57}
58
59#[inline(always)]
63fn get_file_extension(uri: &str) -> Option<&str> {
64 uri.rsplit_once('.').map(|(_, rest)| rest)
65}
66
67#[inline(always)]
68fn get_max_age(uri: &str) -> u64 {
69 let mut max_age = MAX_AGE_ONE_DAY;
71
72 if let Some(extension) = get_file_extension(uri) {
73 if CACHE_EXT_ONE_HOUR.binary_search(&extension).is_ok() {
74 max_age = MAX_AGE_ONE_HOUR;
75 } else if CACHE_EXT_ONE_YEAR.binary_search(&extension).is_ok() {
76 max_age = MAX_AGE_ONE_YEAR;
77 }
78 }
79 max_age
80}
81
82#[cfg(test)]
83mod tests {
84 use hyper::{Body, Response, StatusCode};
85
86 use super::{
87 append_headers, get_file_extension, CACHE_EXT_ONE_HOUR, CACHE_EXT_ONE_YEAR,
88 MAX_AGE_ONE_DAY, MAX_AGE_ONE_HOUR, MAX_AGE_ONE_YEAR,
89 };
90
91 #[test]
92 fn headers_one_hour() {
93 let mut resp = Response::new(Body::empty());
94 *resp.status_mut() = StatusCode::OK;
95
96 for ext in CACHE_EXT_ONE_HOUR.iter() {
97 append_headers(&["/some.", ext].concat(), &mut resp);
98
99 let cache_control = resp.headers().get(http::header::CACHE_CONTROL).unwrap();
100 assert_eq!(resp.status(), StatusCode::OK);
101 assert_eq!(
102 cache_control.to_str().unwrap(),
103 format!("public, max-age={MAX_AGE_ONE_HOUR}")
104 );
105 }
106 }
107
108 #[test]
109 fn headers_one_day_default() {
110 let mut resp = Response::new(Body::empty());
111 *resp.status_mut() = StatusCode::OK;
112
113 append_headers("/", &mut resp);
114
115 let cache_control = resp.headers().get(http::header::CACHE_CONTROL).unwrap();
116 assert_eq!(resp.status(), StatusCode::OK);
117 assert_eq!(
118 cache_control.to_str().unwrap(),
119 format!("public, max-age={MAX_AGE_ONE_DAY}")
120 );
121 }
122
123 #[test]
124 fn headers_one_year() {
125 let mut resp = Response::new(Body::empty());
126 *resp.status_mut() = StatusCode::OK;
127
128 for ext in CACHE_EXT_ONE_YEAR.iter() {
129 append_headers(&["/some.", ext].concat(), &mut resp);
130
131 let cache_control = resp.headers().get(http::header::CACHE_CONTROL).unwrap();
132 assert_eq!(resp.status(), StatusCode::OK);
133 assert_eq!(
134 cache_control.to_str().unwrap(),
135 format!("public, max-age={MAX_AGE_ONE_YEAR}")
136 );
137 }
138 }
139
140 #[test]
141 fn find_uri_extension() {
142 assert_eq!(get_file_extension("/potato.zip"), Some("zip"));
143 assert_eq!(get_file_extension("/potato."), Some(""));
144 assert_eq!(get_file_extension("/"), None);
145 }
146}