warp
warp copied to clipboard
Add a option to control compression level
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
Localhost network time
Brotli level 1 / Custom code
External load time
Localhost network time
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 map
ing 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.
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()
.
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.