1#![doc = include_str!("../README.md")]
2
3use bytes::Bytes;
4use 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 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}