warp icon indicating copy to clipboard operation
warp copied to clipboard

Add a option to control compression level

Open JonathanxD opened this issue 2 years ago • 2 comments

Is your feature request related to a problem? Please describe. There are different levels of brotli compression, ranging from 1 (fastest / less efficient / bigger sizes) to 11 (slower / most efficient / smaller sizes). warp defaults to the default compression level, for Brotli, the default compression level is the highest level, this hugely affects the load times.

Brotli level 11 / Warp Default

External load time

with brotli default

Localhost network time

localhost net with brotli default

Brotli level 1 / Custom code

External load time

with custom brotli

Localhost network time

localhost net with custom brotli

Current workaround

I've tried to write my own Compression implementation to control the level, however, I found that it is not possible since Wrap is a sealed Trait, then I experimented with a simple maping function, it worked.

But it was not portable, I wanted something to just plug in my filter, so I tried to convert it to a function to pass to warp::wrap_fn, but since I can't implement Fn trait yet and there is no currying in Rust (also even if I could implement Fn, I would need to find a way to resolve unconstrained opaque type in trait associated types), I ended up with this function:

use async_compression::tokio::bufread::BrotliEncoder;
use futures_util::TryStreamExt;
use std::io::{Error, ErrorKind};
use tokio_util::io::{ReaderStream, StreamReader};
use warp::http::HeaderValue;
use warp::hyper::header::CONTENT_ENCODING;
use warp::hyper::header::CONTENT_LENGTH;
use warp::hyper::Body;
use warp::reply::Response;
use warp::{Filter, Reply};

pub fn brotli<F, T, E>(
    level: async_compression::Level,
    filter: F,
) -> impl Filter<Extract = (Response,), Error = E> + Clone + Send
where
    F: Filter<Extract = (T,), Error = E> + Clone + Send,
    F::Extract: Reply,
    T: Reply,
{
    filter.map(move |r: T| {
        let r = r.into_response();
        let (mut parts, body) = r.into_parts();

        let s = Body::wrap_stream(ReaderStream::new(BrotliEncoder::with_quality(
            StreamReader::new(TryStreamExt::map_err(body, |e| {
                Error::new(ErrorKind::InvalidData, e)
            })),
            level,
        )));

        parts
            .headers
            .insert(CONTENT_ENCODING, HeaderValue::from_static("br"));

        parts.headers.remove(CONTENT_LENGTH);

        Response::from_parts(parts, s)
    })
}

And since there is no currying in Rust, I need to use it like this:

let ny = warp::path("foo")
    .map(|| warp::reply())
    .with(warp::wrap_fn(|r| compression::brotli(async_compression::Level::Precise(1), r)));

However, this is only a workaround and features a lot of code duplicated from warp.

Describe the solution you'd like It would be extremely handy to have a parameter that controls the compression level, it could be the async-compression Level re-exported.

JonathanxD avatar Apr 17 '22 03:04 JonathanxD

I agree, it would be cool to add a compression builder, kinda like cors has. We could probably expose both, and make the new one be warp::compression::builder().

seanmonstar avatar Apr 17 '22 14:04 seanmonstar

I had a similar problem where I wanted to control whether or not compression happens at all from within my request handler, as that's where I know whether there's any point of compressing the response or not. I ended up returning a flag along with the json result followed by a filter that added a header if the flag was set and finally patched the code in compression.rs to check for this header before wrapping the stream in a gzipencoder... Probably not the most elegant way, but at least it solved my problem.

danishdynamite avatar Oct 20 '22 16:10 danishdynamite