tower_minify_html/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use bytes::Bytes;
4// use futures::future::BoxFuture;
5use http::{Request, Response, header};
6use http_body_util::{BodyExt, Full, combinators::UnsyncBoxBody};
7use std::{
8    pin::Pin,
9    task::{Context, Poll},
10};
11use tower::{Layer, Service};
12use tracing::{debug, error};
13
14pub use minify_html::Cfg;
15
16#[derive(Clone)]
17pub struct MinifyHtmlLayer {
18    config: Cfg,
19}
20
21impl MinifyHtmlLayer {
22    pub fn new(config: Cfg) -> Self {
23        Self { config }
24    }
25}
26
27impl<S> Layer<S> for MinifyHtmlLayer {
28    type Service = MinifyHtml<S>;
29
30    fn layer(&self, inner: S) -> Self::Service {
31        MinifyHtml {
32            inner,
33            config: self.config.clone(),
34        }
35    }
36}
37
38#[derive(Clone)]
39pub struct MinifyHtml<S> {
40    inner: S,
41    config: Cfg,
42}
43
44impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for MinifyHtml<S>
45where
46    S: Service<Request<ReqBody>, Response = Response<ResBody>> + Send + 'static,
47    S::Future: Send + 'static,
48    ResBody: BodyExt<Data = Bytes> + Send + 'static,
49    ResBody::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
50{
51    type Response = Response<UnsyncBoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>>>;
52    type Error = S::Error;
53    type Future =
54        Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
55
56    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
57        self.inner.poll_ready(cx)
58    }
59
60    fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
61        let response_future = self.inner.call(request);
62        let config = self.config.clone();
63
64        Box::pin(async move {
65            let response = response_future.await?;
66
67            let is_html = response
68                .headers()
69                .get(header::CONTENT_TYPE)
70                .and_then(|v| v.to_str().ok())
71                .map(|v| v.contains("text/html"))
72                .unwrap_or(false);
73
74            if !is_html {
75                return Ok(response.map(|b| b.map_err(|e| e.into()).boxed_unsync()));
76            }
77
78            let (mut parts, body) = response.into_parts();
79
80            // Remove Content-Length as it changes
81            parts.headers.remove(header::CONTENT_LENGTH);
82
83            let bytes = match body.collect().await {
84                Ok(c) => c.to_bytes(),
85                Err(_e) => {
86                    error!("Failed to collect response body for minification");
87                    return Ok(Response::builder()
88                        .status(500)
89                        .body(
90                            Full::from("Error processing response body")
91                                .map_err(|e| e.into())
92                                .boxed_unsync(),
93                        )
94                        .unwrap());
95                }
96            };
97
98            let minified = minify_html::minify(&bytes, &config);
99            debug!(
100                "HTML minified: original size {} bytes, minified size {} bytes",
101                bytes.len(),
102                minified.len()
103            );
104
105            let new_body = Full::new(Bytes::from(minified))
106                .map_err(|_e| unreachable!("Full body never errors"))
107                .boxed_unsync();
108
109            Ok(Response::from_parts(parts, new_body))
110        })
111    }
112}