Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: http_only compression middleware #1133

Open
BingHuang-Chin opened this issue May 24, 2023 · 4 comments
Open

Feature request: http_only compression middleware #1133

BingHuang-Chin opened this issue May 24, 2023 · 4 comments

Comments

@BingHuang-Chin
Copy link

Currently using jsonrpsee http_only server and wanted to compress the response before returning to the client using tower-http CompressionLayer.

referencing: CorsLayer

use tower_http::compression::CompressionLayer;

let service_builder = ServiceBuilder::new()
        .layer(ProxyGetRequestLayer::new("/health", "system_health")?)
        .layer(CompressionLayer::new());

let server = ServerBuilder::default()
        .http_only()
        .set_host_filtering(AllowHosts::Any)
        .set_middleware(service_builder)
        .build("127.0.0.1:3000".parse::<SocketAddr>()?)
        .await?;

let mut module = RpcModule::new(());
/* register methods code block */

let handle = server.start(module)?;

After adding CompressionLayer it throw the following errors

55 |     let handle = server.start(module)?;
   |                         ^^^^^ method cannot be called due to unsatisfied trait bounds
   |
   |
62 | pub struct ProxyGetRequest<S> {
   | -----------------------------
   | |
   | doesn't satisfy `<_ as Service<Request<Body>>>::Error = Box<(dyn Error + Send + Sync + 'static)>`
   | doesn't satisfy `<_ as Service<Request<Body>>>::Response = Response<_>`
   | doesn't satisfy `_: Service<Request<Body>>`
   |
16 | pub struct Compression<S, P = DefaultPredicate> {
   | ----------------------------------------------- doesn't satisfy `<_ as Service<Request<Body>>>::Response = Response<Body>`
   |
   = note: the following trait bounds were not satisfied:
           `<ProxyGetRequest<Compression<jsonrpsee::jsonrpsee_server::server::TowerService<()>>> as Service<http::request::Request<jsonrpsee::jsonrpsee_server::logger::Body>>>::Response = http::response::Response<_>`
           `<ProxyGetRequest<Compression<jsonrpsee::jsonrpsee_server::server::TowerService<()>>> as Service<http::request::Request<jsonrpsee::jsonrpsee_server::logger::Body>>>::Error = Box<(dyn StdError + std::marker::Send + std::marker::Sync + 'static)>`
           `ProxyGetRequest<Compression<jsonrpsee::jsonrpsee_server::server::TowerService<()>>>: Service<http::request::Request<jsonrpsee::jsonrpsee_server::logger::Body>>`
           `<Compression<jsonrpsee::jsonrpsee_server::server::TowerService<()>> as Service<http::request::Request<jsonrpsee::jsonrpsee_server::logger::Body>>>::Response = http::response::Response<jsonrpsee::jsonrpsee_server::logger::Body>`
@BingHuang-Chin
Copy link
Author

Did a quick workaround by creating another middleware referencing ProxyGetRequestLayer.

// compression_layer.rs

use flate2::write::GzEncoder;
use flate2::Compression;
use hyper::body::Bytes;
use hyper::{Body, Request, Response};
use jsonrpsee::core::error::Error as RpcError;
use std::error::Error;
use std::future::Future;
use std::io::Write;
use std::pin::Pin;
use std::task::{Context, Poll};
use tower::{Layer, Service};

#[derive(Debug, Clone)]
pub struct CompressionLayer {}

impl CompressionLayer {
    pub fn new() -> Result<Self, RpcError> {
        Ok(Self {})
    }
}
impl<S> Layer<S> for CompressionLayer {
    type Service = CompressionRequest<S>;

    fn layer(&self, inner: S) -> Self::Service {
        CompressionRequest::new(inner).expect("Path already validated in CompressionLayer; qed")
    }
}

#[derive(Debug, Clone)]
pub struct CompressionRequest<S> {
    inner: S,
}

impl<S> CompressionRequest<S> {
    pub fn new(inner: S) -> Result<Self, RpcError> {
        Ok(Self { inner })
    }
}

impl<S> Service<Request<Body>> for CompressionRequest<S>
where
    S: Service<Request<Body>, Response = Response<Body>>,
    S::Response: 'static,
    S::Error: Into<Box<dyn Error + Send + Sync>> + 'static,
    S::Future: Send + 'static,
{
    type Response = S::Response;
    type Error = Box<dyn Error + Send + Sync + 'static>;
    type Future =
        Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;

    #[inline]
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx).map_err(Into::into)
    }

    fn call(&mut self, req: Request<Body>) -> Self::Future {
        let fut = self.inner.call(req);

        let res_fut = async move {
            let res = fut.await.map_err(|err| err.into())?;

            let body = res.into_body();
            let bytes = hyper::body::to_bytes(body).await?;

            let new_res = hyper::Response::builder()
                .status(hyper::StatusCode::OK)
                .header(
                    "content-type",
                    hyper::header::HeaderValue::from_static("application/json"),
                )
                .header(
                    "content-encoding",
                    hyper::header::HeaderValue::from_static("gzip"),
                )
                .body(hyper::Body::from(compress_body(bytes).await?))
                .expect("Unable to parse response body for type conversion");

            Ok(new_res)
        };

        Box::pin(res_fut)
    }
}

async fn compress_body(bytes: Bytes) -> anyhow::Result<Vec<u8>> {
    let compressed_body = tokio::spawn(async move {
        let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
        encoder.write_all(&bytes).unwrap();
        encoder.finish().unwrap()
    })
    .await?;

    Ok(compressed_body)
}

@niklasad1
Copy link
Member

All right, thanks for reporting.

I think the jsonrpsee's trait bounds are too strict which assumes hyper::Request<hyper::Body> and that doesn't seems to work for the CompressionLayer.

So it won't work without the ProxyGetRequestLayer right?

@BingHuang-Chin
Copy link
Author

BingHuang-Chin commented May 25, 2023

If the layers are placed differently i'll get a different error

// CompressionLayer first before ProxyGetRequestLayer

let service_builder = ServiceBuilder::new()
        .layer(CompressionLayer::new())
        .layer(ProxyGetRequestLayer::new("/health", "system_health")?);

Build error

error[E0277]: the size for values of type `(dyn StdError + std::marker::Send + std::marker::Sync + 'static)` cannot be known at compilation time
   |
56 |     let handle = server.start(module)?;
   |                         ^^^^^ doesn't have a size known at compile-time
   |
   = help: the trait `Sized` is not implemented for `(dyn StdError + std::marker::Send + std::marker::Sync + 'static)`
   = help: the trait `StdError` is implemented for `Box<T>`
   = note: required for `Box<(dyn StdError + std::marker::Send + std::marker::Sync + 'static)>` to implement `StdError`
note: required by a bound in `jsonrpsee::jsonrpsee_server::Server::<S, L>::start`
   |
99 |     <B as HttpBody>::Error: Send + Sync + StdError,
   |                                           ^^^^^^^^ required by this bound in `Server::<S, L>::start`

@niklasad1
Copy link
Member

Yeah, error type is probably not StdErr/Box then...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants