forest/rpc/
compression_layer.rs1use std::{
11 env,
12 future::Future,
13 pin::Pin,
14 sync::LazyLock,
15 task::{Context, Poll},
16};
17
18use jsonrpsee::server::{HttpBody, HttpRequest, HttpResponse};
19use tower::{Layer, Service};
20use tower_http::compression::predicate::SizeAbove;
21use tower_http::compression::{Compression, CompressionLayer as TowerCompressionLayer};
22
23const COMPRESS_MIN_BODY_SIZE_VAR: &str = "FOREST_RPC_COMPRESS_MIN_BODY_SIZE";
24
25pub(crate) static COMPRESS_MIN_BODY_SIZE: LazyLock<Option<u64>> = LazyLock::new(|| {
37 parse_compress_min_body_size(env::var(COMPRESS_MIN_BODY_SIZE_VAR).ok().as_deref())
38});
39
40fn parse_compress_min_body_size(raw: Option<&str>) -> Option<u64> {
47 let Ok(parsed) = raw?.parse::<i128>() else {
51 tracing::warn!(
52 "{COMPRESS_MIN_BODY_SIZE_VAR}={raw:?} is not a valid integer; \
53 disabling compression"
54 );
55 return None;
56 };
57 if parsed < 0 {
58 return None;
59 }
60 let max = i128::from(u64::MAX);
61 if parsed > max {
62 tracing::warn!(
63 "{COMPRESS_MIN_BODY_SIZE_VAR}={parsed} exceeds the maximum of {max}; \
64 clamping to {max} bytes"
65 );
66 }
67 Some(u64::try_from(parsed.min(max)).expect("bounded above to u64::MAX"))
69}
70
71#[derive(Clone)]
73pub(crate) struct CompressionLayer {
74 inner: TowerCompressionLayer<SizeAbove>,
75}
76
77impl CompressionLayer {
78 pub(crate) fn new(min_body_size: u64) -> Self {
80 Self {
81 inner: TowerCompressionLayer::new().compress_when(SizeAbove::new(min_body_size)),
82 }
83 }
84}
85
86impl<S> Layer<S> for CompressionLayer {
87 type Service = CompressionService<S>;
88
89 fn layer(&self, inner: S) -> Self::Service {
90 CompressionService {
91 inner: self.inner.layer(inner),
92 }
93 }
94}
95
96#[derive(Clone)]
97pub(crate) struct CompressionService<S> {
98 inner: Compression<S, SizeAbove>,
99}
100
101impl<S, ReqBody> Service<HttpRequest<ReqBody>> for CompressionService<S>
102where
103 S: Service<HttpRequest<ReqBody>, Response = HttpResponse>,
104 S::Future: Send + 'static,
105{
106 type Response = HttpResponse;
107 type Error = S::Error;
108 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
109
110 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
111 self.inner.poll_ready(cx)
112 }
113
114 fn call(&mut self, req: HttpRequest<ReqBody>) -> Self::Future {
115 let fut = self.inner.call(req);
116 Box::pin(async move {
117 let resp = fut.await?;
119 let (parts, compressed_body) = resp.into_parts();
120 Ok(Self::Response::from_parts(
121 parts,
122 HttpBody::new(compressed_body),
123 ))
124 })
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use http::header::{ACCEPT_ENCODING, CONTENT_ENCODING};
132 use std::{convert::Infallible, future::ready};
133
134 const TEST_DATA: &str = "cthulhu fhtagn ";
135 const REPEAT_COUNT: usize = 1000;
136
137 #[derive(Clone)]
138 struct MockService;
139
140 impl Service<HttpRequest> for MockService {
141 type Response = HttpResponse;
142 type Error = Infallible;
143 type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
144
145 fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
146 Poll::Ready(Ok(()))
147 }
148
149 fn call(&mut self, _: HttpRequest) -> Self::Future {
150 let body = HttpBody::from(TEST_DATA.repeat(REPEAT_COUNT));
151 ready(Ok(HttpResponse::builder().body(body).unwrap()))
152 }
153 }
154
155 async fn body_size(resp: HttpResponse) -> usize {
156 let body = axum::body::Body::new(resp.into_body());
157 axum::body::to_bytes(body, usize::MAX).await.unwrap().len()
158 }
159
160 fn uncompressed_size() -> usize {
161 TEST_DATA.repeat(REPEAT_COUNT).len()
162 }
163
164 #[tokio::test]
165 async fn gzip_compresses_when_requested() {
166 let mut svc = CompressionLayer::new(0).layer(MockService);
167 let req = HttpRequest::builder()
168 .header(ACCEPT_ENCODING, "gzip")
169 .body(HttpBody::empty())
170 .unwrap();
171 let resp = svc.call(req).await.unwrap();
172 assert_eq!(resp.headers().get(CONTENT_ENCODING).unwrap(), "gzip");
173 assert!(body_size(resp).await < uncompressed_size());
174 }
175
176 #[tokio::test]
177 async fn passthrough_when_encoding_not_requested() {
178 let mut svc = CompressionLayer::new(0).layer(MockService);
179 let req = HttpRequest::builder().body(HttpBody::empty()).unwrap();
180 let resp = svc.call(req).await.unwrap();
181 assert!(resp.headers().get(CONTENT_ENCODING).is_none());
182 assert_eq!(body_size(resp).await, uncompressed_size());
183 }
184
185 #[tokio::test]
186 async fn below_threshold_is_not_compressed() {
187 let mut svc = CompressionLayer::new(u64::MAX).layer(MockService);
188 let req = HttpRequest::builder()
189 .header(ACCEPT_ENCODING, "gzip")
190 .body(HttpBody::empty())
191 .unwrap();
192 let resp = svc.call(req).await.unwrap();
193 assert!(resp.headers().get(CONTENT_ENCODING).is_none());
194 assert_eq!(body_size(resp).await, uncompressed_size());
195 }
196
197 #[test]
198 fn parse_unset_disables() {
199 assert_eq!(parse_compress_min_body_size(None), None);
200 }
201
202 #[test]
203 fn parse_negative_disables() {
204 assert_eq!(parse_compress_min_body_size(Some("-1")), None);
205 assert_eq!(parse_compress_min_body_size(Some("-999999")), None);
206 assert_eq!(parse_compress_min_body_size(Some("-2147483648")), None); assert_eq!(
209 parse_compress_min_body_size(Some("-9223372036854775808")),
210 None
211 ); }
213
214 #[test]
215 fn parse_accepts_in_range_values() {
216 assert_eq!(parse_compress_min_body_size(Some("0")), Some(0));
217 assert_eq!(parse_compress_min_body_size(Some("512")), Some(512));
218 assert_eq!(parse_compress_min_body_size(Some("1024")), Some(1024));
219 assert_eq!(
220 parse_compress_min_body_size(Some("18446744073709551615")),
221 Some(u64::MAX)
222 );
223 }
224
225 #[test]
226 fn parse_clamps_above_u64_max() {
227 assert_eq!(
228 parse_compress_min_body_size(Some("18446744073709551616")), Some(u64::MAX)
230 );
231 assert_eq!(
232 parse_compress_min_body_size(Some("118446744073709551616")),
233 Some(u64::MAX)
234 );
235 assert_eq!(
236 parse_compress_min_body_size(Some("170141183460469231731687303715884105727")), Some(u64::MAX)
238 );
239 }
240
241 #[test]
242 fn parse_invalid_disables() {
243 assert_eq!(parse_compress_min_body_size(Some("")), None);
244 assert_eq!(parse_compress_min_body_size(Some("lots")), None);
245 assert_eq!(parse_compress_min_body_size(Some("1.5")), None);
246 }
247}