Skip to main content

rust_web_server/compression/
mod.rs

1#[cfg(test)]
2mod tests;
3
4use flate2::Compression;
5use flate2::write::GzEncoder;
6use std::io::Write;
7use crate::header::Header;
8use crate::request::Request;
9use crate::response::Response;
10
11/// MIME types whose responses are worth compressing.
12const COMPRESSIBLE: &[&str] = &[
13    "text/html",
14    "text/css",
15    "text/javascript",
16    "text/plain",
17    "text/xml",
18    "application/json",
19    "application/javascript",
20    "application/xml",
21    "application/xhtml+xml",
22    "image/svg+xml",
23];
24
25/// If the client accepts gzip and the response body is compressible text,
26/// compress every content range in-place and add `Content-Encoding: gzip`.
27/// Also appends `Accept-Encoding` to the `Vary` header.
28pub fn apply_gzip(request: &Request, response: &mut Response) {
29    if response.content_range_list.is_empty() {
30        return;
31    }
32
33    let accepts_gzip = request
34        .get_header(Header::_ACCEPT_ENCODING.to_string())
35        .map(|h| h.value.to_lowercase().contains("gzip"))
36        .unwrap_or(false);
37
38    if !accepts_gzip {
39        return;
40    }
41
42    let content_type = response
43        .content_range_list
44        .first()
45        .map(|cr| cr.content_type.to_lowercase())
46        .unwrap_or_default();
47
48    let is_compressible = COMPRESSIBLE.iter().any(|mime| content_type.starts_with(mime));
49    if !is_compressible {
50        return;
51    }
52
53    let mut compressed_ok = true;
54    for cr in &mut response.content_range_list {
55        let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
56        if encoder.write_all(&cr.body).is_err() {
57            compressed_ok = false;
58            break;
59        }
60        match encoder.finish() {
61            Ok(compressed) => {
62                let new_len = compressed.len() as u64;
63                cr.body = compressed;
64                cr.range.end = new_len;
65                cr.size = new_len.to_string();
66            }
67            Err(_) => {
68                compressed_ok = false;
69                break;
70            }
71        }
72    }
73
74    if !compressed_ok {
75        return;
76    }
77
78    response.headers.push(Header {
79        name: Header::_CONTENT_ENCODING.to_string(),
80        value: "gzip".to_string(),
81    });
82
83    // append Accept-Encoding to Vary, or add it
84    let vary_pos = response.headers.iter().position(|h| h.name == Header::_VARY);
85    if let Some(i) = vary_pos {
86        let current = response.headers[i].value.clone();
87        if !current.to_lowercase().contains("accept-encoding") {
88            response.headers[i].value = format!("{}, Accept-Encoding", current);
89        }
90    } else {
91        response.headers.push(Header {
92            name: Header::_VARY.to_string(),
93            value: "Accept-Encoding".to_string(),
94        });
95    }
96}