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 future::Future,
9 pin::Pin,
10 task::{Context, Poll},
11};
12use tower::{Layer, Service};
13use tracing::{debug, error};
14
15#[cfg(feature = "standard")]
16pub use minify_html::Cfg;
17
18#[cfg(feature = "onepass")]
19pub use minify_html_onepass::Cfg as OnePassCfg;
20
21#[cfg(not(any(feature = "standard", feature = "onepass")))]
22compile_error!("Either feature 'standard' or 'onepass' (or both) must be enabled");
23
24#[derive(Clone, Copy, Debug)]
26pub enum Backend {
27 #[cfg(feature = "standard")]
29 Standard,
30 #[cfg(feature = "onepass")]
32 Onepass,
33}
34
35impl Default for Backend {
36 fn default() -> Self {
37 #[cfg(feature = "standard")]
38 return Backend::Standard;
39 #[cfg(all(feature = "onepass", not(feature = "standard")))]
40 return Backend::Onepass;
41 }
42}
43
44#[derive(Clone)]
46pub struct MinifyHtmlLayer {
47 backend: Backend,
48 #[cfg(feature = "standard")]
49 standard_config: minify_html::Cfg,
50 #[cfg(feature = "onepass")]
51 onepass_config: std::sync::Arc<minify_html_onepass::Cfg>,
53}
54
55impl MinifyHtmlLayer {
56 pub fn builder() -> MinifyHtmlLayerBuilder {
58 MinifyHtmlLayerBuilder::default()
59 }
60
61 #[cfg(feature = "standard")]
63 pub fn new(config: minify_html::Cfg) -> Self {
64 Self::builder().standard_config(config).build()
65 }
66}
67
68#[derive(Default)]
70pub struct MinifyHtmlLayerBuilder {
71 backend: Backend,
72 #[cfg(feature = "standard")]
73 standard_config: minify_html::Cfg,
74 #[cfg(feature = "onepass")]
75 onepass_config: minify_html_onepass::Cfg,
76}
77
78impl MinifyHtmlLayerBuilder {
79 pub fn backend(mut self, backend: Backend) -> Self {
81 self.backend = backend;
82 self
83 }
84
85 #[cfg(feature = "standard")]
87 pub fn standard_config(mut self, config: minify_html::Cfg) -> Self {
88 self.standard_config = config;
89 self
90 }
91
92 #[cfg(feature = "onepass")]
94 pub fn onepass_config(mut self, config: minify_html_onepass::Cfg) -> Self {
95 self.onepass_config = config;
96 self
97 }
98
99 pub fn build(self) -> MinifyHtmlLayer {
101 MinifyHtmlLayer {
102 backend: self.backend,
103 #[cfg(feature = "standard")]
104 standard_config: self.standard_config,
105 #[cfg(feature = "onepass")]
106 onepass_config: std::sync::Arc::new(self.onepass_config),
107 }
108 }
109}
110
111impl<S> Layer<S> for MinifyHtmlLayer {
112 type Service = MinifyHtml<S>;
113
114 fn layer(&self, inner: S) -> Self::Service {
115 MinifyHtml {
116 inner,
117 backend: self.backend,
118 #[cfg(feature = "standard")]
119 standard_config: self.standard_config.clone(),
120 #[cfg(feature = "onepass")]
121 onepass_config: self.onepass_config.clone(),
122 }
123 }
124}
125
126#[derive(Clone)]
128pub struct MinifyHtml<S> {
129 inner: S,
130 backend: Backend,
131 #[cfg(feature = "standard")]
132 standard_config: minify_html::Cfg,
133 #[cfg(feature = "onepass")]
134 onepass_config: std::sync::Arc<minify_html_onepass::Cfg>,
135}
136
137impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for MinifyHtml<S>
138where
139 S: Service<Request<ReqBody>, Response = Response<ResBody>> + Send + 'static,
140 S::Future: Send + 'static,
141 ResBody: BodyExt<Data = Bytes> + Send + 'static,
142 ResBody::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
143{
144 type Response = Response<UnsyncBoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>>>;
145 type Error = S::Error;
146 type Future =
147 Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
148
149 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
150 self.inner.poll_ready(cx)
151 }
152
153 fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
154 let response_future = self.inner.call(request);
155 let backend = self.backend;
156 #[cfg(feature = "standard")]
157 let standard_config = self.standard_config.clone();
158 #[cfg(feature = "onepass")]
159 let onepass_config = self.onepass_config.clone();
160
161 Box::pin(async move {
162 let response = response_future.await?;
163
164 let is_html = response
165 .headers()
166 .get(header::CONTENT_TYPE)
167 .and_then(|v| v.to_str().ok())
168 .map(|v| v.contains("text/html"))
169 .unwrap_or(false);
170
171 if !is_html {
172 return Ok(response.map(|b| b.map_err(|e| e.into()).boxed_unsync()));
173 }
174
175 let (mut parts, body) = response.into_parts();
176
177 parts.headers.remove(header::CONTENT_LENGTH);
179
180 let bytes = match body.collect().await {
181 Ok(c) => c.to_bytes(),
182 Err(_e) => {
183 error!("Failed to collect response body for minification");
184 return Ok(error_500_response());
185 }
186 };
187
188 let minified = match backend {
189 #[cfg(feature = "standard")]
190 Backend::Standard => minify_html::minify(&bytes, &standard_config),
191
192 #[cfg(feature = "onepass")]
193 Backend::Onepass => {
194 let mut vec = bytes.to_vec();
195 match minify_html_onepass::in_place(&mut vec, &onepass_config) {
196 Ok(len) => {
197 vec.truncate(len);
198 vec
199 }
200 Err(_) => return Ok(error_500_response()),
201 }
202 }
203 };
204
205 debug!(
206 "HTML minified: original size {} bytes, minified size {} bytes",
207 bytes.len(),
208 minified.len()
209 );
210
211 let new_body = Full::new(Bytes::from(minified))
212 .map_err(|_e| unreachable!("Full body never errors"))
213 .boxed_unsync();
214
215 Ok(Response::from_parts(parts, new_body))
216 })
217 }
218}
219
220fn error_500_response() -> Response<UnsyncBoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>>>
221{
222 Response::builder()
223 .status(500)
224 .body(
225 Full::from("Internal Server Error")
226 .map_err(|e| e.into())
227 .boxed_unsync(),
228 )
229 .unwrap()
230}