Skip to main content

reth_rpc_layer/
compression_layer.rs

1use jsonrpsee_http_client::{HttpBody, HttpRequest, HttpResponse};
2use std::{
3    future::Future,
4    pin::Pin,
5    task::{Context, Poll},
6};
7use tower::{Layer, Service};
8use tower_http::compression::{Compression, CompressionLayer as TowerCompressionLayer};
9
10/// This layer is a wrapper around [`tower_http::compression::CompressionLayer`] that integrates
11/// with jsonrpsee's HTTP types. It automatically compresses responses based on the client's
12/// `Accept-Encoding` header.
13#[expect(missing_debug_implementations)]
14#[derive(Clone)]
15pub struct CompressionLayer {
16    inner_layer: TowerCompressionLayer,
17}
18
19impl CompressionLayer {
20    /// Creates a new compression layer with zstd, gzip, brotli and deflate enabled.
21    pub fn new() -> Self {
22        Self {
23            inner_layer: TowerCompressionLayer::new().gzip(true).br(true).deflate(true).zstd(true),
24        }
25    }
26
27    /// Creates a compression layer allowing only the listed algorithms.
28    ///
29    /// The client's `Accept-Encoding` quality values determine selection among allowed algorithms;
30    /// ties use `tower_http`'s fixed **zstd > br > gzip > deflate** order. The order of `algos` is
31    /// ignored. Omitted quality values default to 1; if no allowed algorithm is acceptable, the
32    /// response is uncompressed.
33    pub fn with_algorithms(algos: &[impl AsRef<str>]) -> Self {
34        // Clear tower_http's enabled-by-default algorithms to make `algos` an allowlist.
35        let mut layer = TowerCompressionLayer::new().no_zstd().no_gzip().no_deflate().no_br();
36
37        for algo in algos {
38            match algo.as_ref() {
39                "zstd" => layer = layer.zstd(true),
40                "deflate" => layer = layer.deflate(true),
41                "gzip" => layer = layer.gzip(true),
42                "br" | "brotli" => layer = layer.br(true),
43                _ => {}
44            }
45        }
46
47        Self { inner_layer: layer }
48    }
49}
50
51impl Default for CompressionLayer {
52    /// Creates a new compression layer with default settings.
53    /// See [`CompressionLayer::new`] for details.
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl<S> Layer<S> for CompressionLayer {
60    type Service = CompressionService<S>;
61
62    fn layer(&self, inner: S) -> Self::Service {
63        CompressionService { compression: self.inner_layer.layer(inner) }
64    }
65}
66
67/// Service that performs response compression.
68///
69/// Created by [`CompressionLayer`].
70#[expect(missing_debug_implementations)]
71#[derive(Clone)]
72pub struct CompressionService<S> {
73    compression: Compression<S>,
74}
75
76impl<S> Service<HttpRequest> for CompressionService<S>
77where
78    S: Service<HttpRequest, Response = HttpResponse>,
79    S::Future: Send + 'static,
80{
81    type Response = HttpResponse;
82    type Error = S::Error;
83    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
84
85    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
86        self.compression.poll_ready(cx)
87    }
88
89    fn call(&mut self, req: HttpRequest) -> Self::Future {
90        let fut = self.compression.call(req);
91
92        Box::pin(async move {
93            let resp = fut.await?;
94            let (parts, compressed_body) = resp.into_parts();
95            let http_body = HttpBody::new(compressed_body);
96
97            Ok(Self::Response::from_parts(parts, http_body))
98        })
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use http::header::{ACCEPT_ENCODING, CONTENT_ENCODING};
106    use http_body_util::BodyExt;
107    use jsonrpsee_http_client::{HttpRequest, HttpResponse};
108    use std::{convert::Infallible, future::ready};
109
110    const TEST_DATA: &str = "compress test data ";
111    const REPEAT_COUNT: usize = 1000;
112
113    #[derive(Clone)]
114    struct MockRequestService;
115
116    impl Service<HttpRequest> for MockRequestService {
117        type Response = HttpResponse;
118        type Error = Infallible;
119        type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
120
121        fn poll_ready(
122            &mut self,
123            _: &mut std::task::Context<'_>,
124        ) -> std::task::Poll<Result<(), Self::Error>> {
125            std::task::Poll::Ready(Ok(()))
126        }
127
128        fn call(&mut self, _: HttpRequest) -> Self::Future {
129            let body = HttpBody::from(TEST_DATA.repeat(REPEAT_COUNT));
130            let response = HttpResponse::builder().body(body).unwrap();
131            ready(Ok(response))
132        }
133    }
134
135    fn setup_compression_service(
136    ) -> impl Service<HttpRequest, Response = HttpResponse, Error = Infallible> {
137        CompressionLayer::new().layer(MockRequestService)
138    }
139
140    async fn get_response_size(response: HttpResponse) -> usize {
141        // Get the total size of the response body
142        response.into_body().collect().await.unwrap().to_bytes().len()
143    }
144
145    #[tokio::test]
146    async fn test_gzip_compression() {
147        let mut service = setup_compression_service();
148        let request =
149            HttpRequest::builder().header(ACCEPT_ENCODING, "gzip").body(HttpBody::empty()).unwrap();
150
151        let uncompressed_len = TEST_DATA.repeat(REPEAT_COUNT).len();
152
153        // Make the request
154        let response = service.call(request).await.unwrap();
155
156        // Verify the response has gzip content-encoding
157        assert_eq!(
158            response.headers().get(CONTENT_ENCODING).unwrap(),
159            "gzip",
160            "Response should be gzip encoded"
161        );
162
163        // Verify the response body is actually compressed (should be smaller than original)
164        let compressed_size = get_response_size(response).await;
165        assert!(
166            compressed_size < uncompressed_len,
167            "Compressed size ({compressed_size}) should be smaller than original size ({uncompressed_len})"
168        );
169    }
170
171    #[tokio::test]
172    async fn test_no_compression_when_not_requested() {
173        // Create a service with compression
174        let mut service = setup_compression_service();
175        let request = HttpRequest::builder().body(HttpBody::empty()).unwrap();
176
177        let response = service.call(request).await.unwrap();
178        assert!(
179            response.headers().get(CONTENT_ENCODING).is_none(),
180            "Response should not be compressed when not requested"
181        );
182
183        let uncompressed_len = TEST_DATA.repeat(REPEAT_COUNT).len();
184
185        // Verify the response body matches the original size
186        let response_size = get_response_size(response).await;
187        assert!(
188            response_size == uncompressed_len,
189            "Response size ({response_size}) should equal original size ({uncompressed_len})"
190        );
191    }
192}