use super::middleware::Next;
use super::request::Request;
use super::response::{HeaderPair, Response};
use bytes::Bytes;
use flate2::Compression;
use flate2::write::GzEncoder;
use std::borrow::Cow;
use std::future::Future;
use std::io::Write;
use std::pin::Pin;
const MIN_COMPRESS_SIZE: usize = 1024;
fn accepts_gzip(req: &Request) -> bool {
match req.header("accept-encoding") {
Some(val) => val
.split(',')
.any(|enc| enc.trim().eq_ignore_ascii_case("gzip")),
None => false,
}
}
pub fn auto()
-> impl Fn(&Request, Next) -> Pin<Box<dyn Future<Output = Response> + Send>> + Send + Sync + 'static
{
move |req, next| {
let accepts_gzip = accepts_gzip(req);
let resp_fut = next.call(req);
Box::pin(async move {
let resp = resp_fut.await;
match accepts_gzip {
false => resp,
true => try_compress(resp),
}
})
}
}
fn compression_eligibility(headers: &[HeaderPair]) -> (bool, bool) {
let mut compressible = false;
let mut encoded = false;
for (k, v) in headers {
match () {
() if k.eq_ignore_ascii_case("Content-Type") => {
let ct: &str = v.as_ref();
compressible = ct.starts_with("text/")
|| ct.starts_with("application/json")
|| ct.starts_with("application/xml");
}
() if k.eq_ignore_ascii_case("Content-Encoding") => {
encoded = true;
}
() => {}
}
}
(compressible, encoded)
}
fn try_compress(resp: Response) -> Response {
let (compressible, encoded) = compression_eligibility(resp.headers());
match (
compressible,
encoded,
resp.body_bytes().len() >= MIN_COMPRESS_SIZE,
) {
(true, false, true) => compress_gzip(resp),
_ => resp,
}
}
fn is_gzip_skip_header(name: &str) -> bool {
name.eq_ignore_ascii_case("Content-Length")
}
fn compress_gzip(resp: Response) -> Response {
let body = resp.body_bytes();
let mut encoder = GzEncoder::new(Vec::with_capacity(body.len() / 2), Compression::fast());
match encoder.write_all(body).and_then(|()| encoder.finish()) {
Ok(compressed) => {
let status = resp.status();
let mut headers: Vec<HeaderPair> = resp
.headers()
.iter()
.filter(|(k, _)| !is_gzip_skip_header(k))
.cloned()
.collect();
headers.push((Cow::Borrowed("Content-Encoding"), Cow::Borrowed("gzip")));
headers.push((Cow::Borrowed("Vary"), Cow::Borrowed("Accept-Encoding")));
Response::new(status, Bytes::from(compressed), headers)
}
Err(err) => {
tracing::error!(%err, "gzip compression failed");
resp
}
}
}