Brotli support (#791)
This commit is contained in:
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -60,6 +60,7 @@ jobs:
|
|||||||
- "feat.: cookies"
|
- "feat.: cookies"
|
||||||
- "feat.: blocking"
|
- "feat.: blocking"
|
||||||
- "feat.: gzip"
|
- "feat.: gzip"
|
||||||
|
- "feat.: brotli"
|
||||||
- "feat.: json"
|
- "feat.: json"
|
||||||
- "feat.: stream"
|
- "feat.: stream"
|
||||||
- "feat.: socks/default-tls"
|
- "feat.: socks/default-tls"
|
||||||
@@ -82,21 +83,21 @@ jobs:
|
|||||||
- name: windows / stable-x86_64-msvc
|
- name: windows / stable-x86_64-msvc
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
features: "--features blocking,gzip,json"
|
features: "--features blocking,gzip,brotli,json"
|
||||||
- name: windows / stable-i686-msvc
|
- name: windows / stable-i686-msvc
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
target: i686-pc-windows-msvc
|
target: i686-pc-windows-msvc
|
||||||
features: "--features blocking,gzip,json"
|
features: "--features blocking,gzip,brotli,json"
|
||||||
- name: windows / stable-x86_64-gnu
|
- name: windows / stable-x86_64-gnu
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
rust: stable-x86_64-pc-windows-gnu
|
rust: stable-x86_64-pc-windows-gnu
|
||||||
target: x86_64-pc-windows-gnu
|
target: x86_64-pc-windows-gnu
|
||||||
features: "--features blocking,gzip,json"
|
features: "--features blocking,gzip,brotli,json"
|
||||||
- name: windows / stable-i686-gnu
|
- name: windows / stable-i686-gnu
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
rust: stable-i686-pc-windows-gnu
|
rust: stable-i686-pc-windows-gnu
|
||||||
target: i686-pc-windows-gnu
|
target: i686-pc-windows-gnu
|
||||||
features: "--features blocking,gzip,json"
|
features: "--features blocking,gzip,brotli,json"
|
||||||
|
|
||||||
- name: "feat.: default-tls disabled"
|
- name: "feat.: default-tls disabled"
|
||||||
features: "--no-default-features"
|
features: "--no-default-features"
|
||||||
@@ -112,6 +113,8 @@ jobs:
|
|||||||
features: "--features blocking"
|
features: "--features blocking"
|
||||||
- name: "feat.: gzip"
|
- name: "feat.: gzip"
|
||||||
features: "--features gzip"
|
features: "--features gzip"
|
||||||
|
- name: "feat.: brotli"
|
||||||
|
features: "--features brotli"
|
||||||
- name: "feat.: json"
|
- name: "feat.: json"
|
||||||
features: "--features json"
|
features: "--features json"
|
||||||
- name: "feat.: stream"
|
- name: "feat.: stream"
|
||||||
|
|||||||
14
Cargo.toml
14
Cargo.toml
@@ -39,7 +39,9 @@ blocking = ["futures-util/io", "tokio/rt-threaded", "tokio/rt-core", "tokio/sync
|
|||||||
|
|
||||||
cookies = ["cookie_crate", "cookie_store"]
|
cookies = ["cookie_crate", "cookie_store"]
|
||||||
|
|
||||||
gzip = ["async-compression"]
|
gzip = ["async-compression", "async-compression/gzip"]
|
||||||
|
|
||||||
|
brotli = ["async-compression", "async-compression/brotli"]
|
||||||
|
|
||||||
json = ["serde_json"]
|
json = ["serde_json"]
|
||||||
|
|
||||||
@@ -104,8 +106,8 @@ webpki-roots = { version = "0.17", optional = true }
|
|||||||
cookie_crate = { version = "0.12", package = "cookie", optional = true }
|
cookie_crate = { version = "0.12", package = "cookie", optional = true }
|
||||||
cookie_store = { version = "0.10", optional = true }
|
cookie_store = { version = "0.10", optional = true }
|
||||||
|
|
||||||
## gzip
|
## compression
|
||||||
async-compression = { version = "0.2.0", default-features = false, features = ["gzip", "stream"], optional = true }
|
async-compression = { version = "0.3.0", default-features = false, features = ["stream"], optional = true }
|
||||||
|
|
||||||
|
|
||||||
## socks
|
## socks
|
||||||
@@ -119,6 +121,7 @@ env_logger = "0.6"
|
|||||||
hyper = { version = "0.13", default-features = false, features = ["tcp", "stream"] }
|
hyper = { version = "0.13", default-features = false, features = ["tcp", "stream"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
libflate = "0.1"
|
libflate = "0.1"
|
||||||
|
brotli_crate = { package = "brotli", version = "3.3.0" }
|
||||||
doc-comment = "0.3"
|
doc-comment = "0.3"
|
||||||
tokio = { version = "0.2.0", default-features = false, features = ["macros"] }
|
tokio = { version = "0.2.0", default-features = false, features = ["macros"] }
|
||||||
|
|
||||||
@@ -177,3 +180,8 @@ required-features = ["cookies"]
|
|||||||
name = "gzip"
|
name = "gzip"
|
||||||
path = "tests/gzip.rs"
|
path = "tests/gzip.rs"
|
||||||
required-features = ["gzip"]
|
required-features = ["gzip"]
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "brotli"
|
||||||
|
path = "tests/brotli.rs"
|
||||||
|
required-features = ["brotli"]
|
||||||
@@ -11,8 +11,8 @@ use http::header::{
|
|||||||
Entry, HeaderMap, HeaderValue, ACCEPT, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH,
|
Entry, HeaderMap, HeaderValue, ACCEPT, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH,
|
||||||
CONTENT_TYPE, LOCATION, PROXY_AUTHORIZATION, RANGE, REFERER, TRANSFER_ENCODING, USER_AGENT,
|
CONTENT_TYPE, LOCATION, PROXY_AUTHORIZATION, RANGE, REFERER, TRANSFER_ENCODING, USER_AGENT,
|
||||||
};
|
};
|
||||||
use http::Uri;
|
|
||||||
use http::uri::Scheme;
|
use http::uri::Scheme;
|
||||||
|
use http::Uri;
|
||||||
use hyper::client::ResponseFuture;
|
use hyper::client::ResponseFuture;
|
||||||
#[cfg(feature = "native-tls-crate")]
|
#[cfg(feature = "native-tls-crate")]
|
||||||
use native_tls_crate::TlsConnector;
|
use native_tls_crate::TlsConnector;
|
||||||
@@ -58,6 +58,7 @@ pub struct ClientBuilder {
|
|||||||
struct Config {
|
struct Config {
|
||||||
// NOTE: When adding a new field, update `fmt::Debug for ClientBuilder`
|
// NOTE: When adding a new field, update `fmt::Debug for ClientBuilder`
|
||||||
gzip: bool,
|
gzip: bool,
|
||||||
|
brotli: bool,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
hostname_verification: bool,
|
hostname_verification: bool,
|
||||||
@@ -106,6 +107,7 @@ impl ClientBuilder {
|
|||||||
config: Config {
|
config: Config {
|
||||||
error: None,
|
error: None,
|
||||||
gzip: cfg!(feature = "gzip"),
|
gzip: cfg!(feature = "gzip"),
|
||||||
|
brotli: cfg!(feature = "brotli"),
|
||||||
headers,
|
headers,
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
hostname_verification: true,
|
hostname_verification: true,
|
||||||
@@ -179,7 +181,6 @@ impl ClientBuilder {
|
|||||||
cert.add_to_native_tls(&mut tls);
|
cert.add_to_native_tls(&mut tls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
{
|
{
|
||||||
if let Some(id) = config.identity {
|
if let Some(id) = config.identity {
|
||||||
@@ -246,7 +247,9 @@ impl ClientBuilder {
|
|||||||
if let Some(http2_initial_stream_window_size) = config.http2_initial_stream_window_size {
|
if let Some(http2_initial_stream_window_size) = config.http2_initial_stream_window_size {
|
||||||
builder.http2_initial_stream_window_size(http2_initial_stream_window_size);
|
builder.http2_initial_stream_window_size(http2_initial_stream_window_size);
|
||||||
}
|
}
|
||||||
if let Some(http2_initial_connection_window_size) = config.http2_initial_connection_window_size {
|
if let Some(http2_initial_connection_window_size) =
|
||||||
|
config.http2_initial_connection_window_size
|
||||||
|
{
|
||||||
builder.http2_initial_connection_window_size(http2_initial_connection_window_size);
|
builder.http2_initial_connection_window_size(http2_initial_connection_window_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +268,7 @@ impl ClientBuilder {
|
|||||||
#[cfg(feature = "cookies")]
|
#[cfg(feature = "cookies")]
|
||||||
cookie_store: config.cookie_store.map(RwLock::new),
|
cookie_store: config.cookie_store.map(RwLock::new),
|
||||||
gzip: config.gzip,
|
gzip: config.gzip,
|
||||||
|
brotli: config.brotli,
|
||||||
hyper: hyper_client,
|
hyper: hyper_client,
|
||||||
headers: config.headers,
|
headers: config.headers,
|
||||||
redirect_policy: config.redirect_policy,
|
redirect_policy: config.redirect_policy,
|
||||||
@@ -278,7 +282,6 @@ impl ClientBuilder {
|
|||||||
|
|
||||||
// Higher-level options
|
// Higher-level options
|
||||||
|
|
||||||
|
|
||||||
/// Sets the `User-Agent` header to be used by this client.
|
/// Sets the `User-Agent` header to be used by this client.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
@@ -360,7 +363,6 @@ impl ClientBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Enable a persistent cookie store for the client.
|
/// Enable a persistent cookie store for the client.
|
||||||
///
|
///
|
||||||
/// Cookies received in responses will be preserved and included in
|
/// Cookies received in responses will be preserved and included in
|
||||||
@@ -383,7 +385,7 @@ impl ClientBuilder {
|
|||||||
|
|
||||||
/// Enable auto gzip decompression by checking the `Content-Encoding` response header.
|
/// Enable auto gzip decompression by checking the `Content-Encoding` response header.
|
||||||
///
|
///
|
||||||
/// If auto gzip decompresson is turned on:
|
/// If auto gzip decompression is turned on:
|
||||||
///
|
///
|
||||||
/// - When sending a request and if the request's headers do not already contain
|
/// - When sending a request and if the request's headers do not already contain
|
||||||
/// an `Accept-Encoding` **and** `Range` values, the `Accept-Encoding` header is set to `gzip`.
|
/// an `Accept-Encoding` **and** `Range` values, the `Accept-Encoding` header is set to `gzip`.
|
||||||
@@ -403,6 +405,28 @@ impl ClientBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enable auto brotli decompression by checking the `Content-Encoding` response header.
|
||||||
|
///
|
||||||
|
/// If auto brotli decompression is turned on:
|
||||||
|
///
|
||||||
|
/// - When sending a request and if the request's headers do not already contain
|
||||||
|
/// an `Accept-Encoding` **and** `Range` values, the `Accept-Encoding` header is set to `br`.
|
||||||
|
/// The request body is **not** automatically compressed.
|
||||||
|
/// - When receiving a response, if it's headers contain a `Content-Encoding` value that
|
||||||
|
/// equals to `br`, both values `Content-Encoding` and `Content-Length` are removed from the
|
||||||
|
/// headers' set. The response body is automatically decompressed.
|
||||||
|
///
|
||||||
|
/// If the `brotli` feature is turned on, the default option is enabled.
|
||||||
|
///
|
||||||
|
/// # Optional
|
||||||
|
///
|
||||||
|
/// This requires the optional `brotli` feature to be enabled
|
||||||
|
#[cfg(feature = "brotli")]
|
||||||
|
pub fn brotli(mut self, enable: bool) -> ClientBuilder {
|
||||||
|
self.config.brotli = enable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Disable auto response body gzip decompression.
|
/// Disable auto response body gzip decompression.
|
||||||
///
|
///
|
||||||
/// This method exists even if the optional `gzip` feature is not enabled.
|
/// This method exists even if the optional `gzip` feature is not enabled.
|
||||||
@@ -420,6 +444,23 @@ impl ClientBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Disable auto response body brotli decompression.
|
||||||
|
///
|
||||||
|
/// This method exists even if the optional `brotli` feature is not enabled.
|
||||||
|
/// This can be used to ensure a `Client` doesn't use brotli decompression
|
||||||
|
/// even if another dependency were to enable the optional `brotli` feature.
|
||||||
|
pub fn no_brotli(self) -> ClientBuilder {
|
||||||
|
#[cfg(feature = "brotli")]
|
||||||
|
{
|
||||||
|
self.brotli(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "brotli"))]
|
||||||
|
{
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect options
|
// Redirect options
|
||||||
|
|
||||||
/// Set a `RedirectPolicy` for this client.
|
/// Set a `RedirectPolicy` for this client.
|
||||||
@@ -534,7 +575,10 @@ impl ClientBuilder {
|
|||||||
/// Sets the max connection-level flow control for HTTP2
|
/// Sets the max connection-level flow control for HTTP2
|
||||||
///
|
///
|
||||||
/// Default is currently 65,535 but may change internally to optimize for common uses.
|
/// Default is currently 65,535 but may change internally to optimize for common uses.
|
||||||
pub fn http2_initial_connection_window_size(mut self, sz: impl Into<Option<u32>>) -> ClientBuilder {
|
pub fn http2_initial_connection_window_size(
|
||||||
|
mut self,
|
||||||
|
sz: impl Into<Option<u32>>,
|
||||||
|
) -> ClientBuilder {
|
||||||
self.config.http2_initial_connection_window_size = sz.into();
|
self.config.http2_initial_connection_window_size = sz.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -654,7 +698,6 @@ impl ClientBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Force using the Rustls TLS backend.
|
/// Force using the Rustls TLS backend.
|
||||||
///
|
///
|
||||||
/// Since multiple TLS backends can be optionally enabled, this option will
|
/// Since multiple TLS backends can be optionally enabled, this option will
|
||||||
@@ -807,9 +850,21 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.inner.gzip && !headers.contains_key(ACCEPT_ENCODING) && !headers.contains_key(RANGE)
|
let accept_encoding = match (self.inner.gzip, self.inner.brotli) {
|
||||||
|
(true, true) => Some("gzip, br"),
|
||||||
|
(true, false) => Some("gzip"),
|
||||||
|
(false, true) => Some("br"),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if accept_encoding.is_some()
|
||||||
|
&& !headers.contains_key(ACCEPT_ENCODING)
|
||||||
|
&& !headers.contains_key(RANGE)
|
||||||
{
|
{
|
||||||
headers.insert(ACCEPT_ENCODING, HeaderValue::from_static("gzip"));
|
headers.insert(
|
||||||
|
ACCEPT_ENCODING,
|
||||||
|
HeaderValue::from_static(accept_encoding.unwrap()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let uri = expect_uri(&url);
|
let uri = expect_uri(&url);
|
||||||
@@ -834,7 +889,6 @@ impl Client {
|
|||||||
.or(self.inner.request_timeout)
|
.or(self.inner.request_timeout)
|
||||||
.map(|dur| tokio::time::delay_for(dur));
|
.map(|dur| tokio::time::delay_for(dur));
|
||||||
|
|
||||||
|
|
||||||
*req.headers_mut() = headers.clone();
|
*req.headers_mut() = headers.clone();
|
||||||
|
|
||||||
let in_flight = self.inner.hyper.request(req);
|
let in_flight = self.inner.hyper.request(req);
|
||||||
@@ -913,6 +967,7 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
f.field("gzip", &self.gzip);
|
f.field("gzip", &self.gzip);
|
||||||
|
f.field("brotli", &self.brotli);
|
||||||
|
|
||||||
if !self.proxies.is_empty() {
|
if !self.proxies.is_empty() {
|
||||||
f.field("proxies", &self.proxies);
|
f.field("proxies", &self.proxies);
|
||||||
@@ -977,6 +1032,7 @@ struct ClientRef {
|
|||||||
#[cfg(feature = "cookies")]
|
#[cfg(feature = "cookies")]
|
||||||
cookie_store: Option<RwLock<cookie::CookieStore>>,
|
cookie_store: Option<RwLock<cookie::CookieStore>>,
|
||||||
gzip: bool,
|
gzip: bool,
|
||||||
|
brotli: bool,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
hyper: HyperClient,
|
hyper: HyperClient,
|
||||||
redirect_policy: redirect::Policy,
|
redirect_policy: redirect::Policy,
|
||||||
@@ -999,6 +1055,7 @@ impl ClientRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
f.field("gzip", &self.gzip);
|
f.field("gzip", &self.gzip);
|
||||||
|
f.field("brotli", &self.brotli);
|
||||||
|
|
||||||
if !self.proxies.is_empty() {
|
if !self.proxies.is_empty() {
|
||||||
f.field("proxies", &self.proxies);
|
f.field("proxies", &self.proxies);
|
||||||
@@ -1014,15 +1071,12 @@ impl ClientRef {
|
|||||||
|
|
||||||
f.field("default_headers", &self.headers);
|
f.field("default_headers", &self.headers);
|
||||||
|
|
||||||
|
|
||||||
if let Some(ref d) = self.request_timeout {
|
if let Some(ref d) = self.request_timeout {
|
||||||
f.field("timeout", d);
|
f.field("timeout", d);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub(super) struct Pending {
|
pub(super) struct Pending {
|
||||||
inner: PendingInner,
|
inner: PendingInner,
|
||||||
}
|
}
|
||||||
@@ -1227,17 +1281,20 @@ impl Future for PendingRequest {
|
|||||||
debug!("redirect policy disallowed redirection to '{}'", loc);
|
debug!("redirect policy disallowed redirection to '{}'", loc);
|
||||||
}
|
}
|
||||||
redirect::ActionKind::Error(err) => {
|
redirect::ActionKind::Error(err) => {
|
||||||
return Poll::Ready(Err(crate::error::redirect(
|
return Poll::Ready(Err(crate::error::redirect(err, self.url.clone())));
|
||||||
err,
|
|
||||||
self.url.clone(),
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("response '{}' for {}", res.status(), self.url);
|
debug!("response '{}' for {}", res.status(), self.url);
|
||||||
let res = Response::new(res, self.url.clone(), self.client.gzip, self.timeout.take());
|
let res = Response::new(
|
||||||
|
res,
|
||||||
|
self.url.clone(),
|
||||||
|
self.client.gzip,
|
||||||
|
self.client.brotli,
|
||||||
|
self.timeout.take(),
|
||||||
|
);
|
||||||
return Poll::Ready(Ok(res));
|
return Poll::Ready(Ok(res));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,21 @@
|
|||||||
pub(crate) use self::imp::Decoder;
|
pub(crate) use self::imp::Decoder;
|
||||||
|
|
||||||
#[cfg(not(feature = "gzip"))]
|
|
||||||
mod imp {
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::task::{Context, Poll};
|
|
||||||
|
|
||||||
use bytes::Bytes;
|
|
||||||
use futures_core::Stream;
|
|
||||||
use http::HeaderMap;
|
|
||||||
|
|
||||||
use super::super::Body;
|
|
||||||
pub(crate) struct Decoder {
|
|
||||||
inner: super::super::body::ImplStream,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Decoder {
|
|
||||||
#[cfg(feature = "blocking")]
|
|
||||||
pub(crate) fn empty() -> Decoder {
|
|
||||||
Decoder::plain_text(Body::empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A plain text decoder.
|
|
||||||
///
|
|
||||||
/// This decoder will emit the underlying chunks as-is.
|
|
||||||
fn plain_text(body: Body) -> Decoder {
|
|
||||||
Decoder {
|
|
||||||
inner: body.into_stream(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn detect(_: &mut HeaderMap, body: Body, _: bool) -> Decoder {
|
|
||||||
Decoder::plain_text(body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Stream for Decoder {
|
|
||||||
type Item = crate::Result<Bytes>;
|
|
||||||
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
|
||||||
Pin::new(&mut self.inner).poll_next(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "gzip")]
|
|
||||||
mod imp {
|
mod imp {
|
||||||
|
use std::fmt;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
use std::{fmt, mem};
|
|
||||||
|
|
||||||
|
#[cfg(feature = "gzip")]
|
||||||
use async_compression::stream::GzipDecoder;
|
use async_compression::stream::GzipDecoder;
|
||||||
|
|
||||||
|
#[cfg(feature = "brotli")]
|
||||||
|
use async_compression::stream::BrotliDecoder;
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_core::Stream;
|
use futures_core::Stream;
|
||||||
use futures_util::stream::Peekable;
|
use futures_util::stream::Peekable;
|
||||||
use http::header::{CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING};
|
|
||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
use log::warn;
|
|
||||||
|
|
||||||
use super::super::Body;
|
use super::super::Body;
|
||||||
use crate::error;
|
use crate::error;
|
||||||
@@ -68,17 +27,32 @@ mod imp {
|
|||||||
inner: Inner,
|
inner: Inner,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum DecoderType {
|
||||||
|
#[cfg(feature = "gzip")]
|
||||||
|
Gzip,
|
||||||
|
#[cfg(feature = "brotli")]
|
||||||
|
Brotli,
|
||||||
|
}
|
||||||
|
|
||||||
enum Inner {
|
enum Inner {
|
||||||
/// A `PlainText` decoder just returns the response content as is.
|
/// A `PlainText` decoder just returns the response content as is.
|
||||||
PlainText(super::super::body::ImplStream),
|
PlainText(super::super::body::ImplStream),
|
||||||
|
|
||||||
/// A `Gzip` decoder will uncompress the gzipped response content before returning it.
|
/// A `Gzip` decoder will uncompress the gzipped response content before returning it.
|
||||||
|
#[cfg(feature = "gzip")]
|
||||||
Gzip(GzipDecoder<Peekable<IoStream>>),
|
Gzip(GzipDecoder<Peekable<IoStream>>),
|
||||||
|
|
||||||
|
/// A `Brotli` decoder will uncompress the brotlied response content before returning it.
|
||||||
|
#[cfg(feature = "brotli")]
|
||||||
|
Brotli(BrotliDecoder<Peekable<IoStream>>),
|
||||||
|
|
||||||
/// A decoder that doesn't have a value yet.
|
/// A decoder that doesn't have a value yet.
|
||||||
|
#[cfg(any(feature = "brotli", feature = "gzip"))]
|
||||||
Pending(Pending),
|
Pending(Pending),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A future attempt to poll the response body for EOF so we know whether to use gzip or not.
|
/// A future attempt to poll the response body for EOF so we know whether to use gzip or not.
|
||||||
struct Pending(Peekable<IoStream>);
|
struct Pending(Peekable<IoStream>, DecoderType);
|
||||||
|
|
||||||
struct IoStream(super::super::body::ImplStream);
|
struct IoStream(super::super::body::ImplStream);
|
||||||
|
|
||||||
@@ -108,24 +82,38 @@ mod imp {
|
|||||||
/// A gzip decoder.
|
/// A gzip decoder.
|
||||||
///
|
///
|
||||||
/// This decoder will buffer and decompress chunks that are gzipped.
|
/// This decoder will buffer and decompress chunks that are gzipped.
|
||||||
|
#[cfg(feature = "gzip")]
|
||||||
fn gzip(body: Body) -> Decoder {
|
fn gzip(body: Body) -> Decoder {
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
|
|
||||||
Decoder {
|
Decoder {
|
||||||
inner: Inner::Pending(Pending(IoStream(body.into_stream()).peekable())),
|
inner: Inner::Pending(Pending(
|
||||||
|
IoStream(body.into_stream()).peekable(),
|
||||||
|
DecoderType::Gzip,
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a Decoder from a hyper request.
|
/// A brotli decoder.
|
||||||
///
|
///
|
||||||
/// A decoder is just a wrapper around the hyper request that knows
|
/// This decoder will buffer and decompress chunks that are brotlied.
|
||||||
/// how to decode the content body of the request.
|
#[cfg(feature = "brotli")]
|
||||||
///
|
fn brotli(body: Body) -> Decoder {
|
||||||
/// Uses the correct variant by inspecting the Content-Encoding header.
|
use futures_util::StreamExt;
|
||||||
pub(crate) fn detect(headers: &mut HeaderMap, body: Body, check_gzip: bool) -> Decoder {
|
|
||||||
if !check_gzip {
|
Decoder {
|
||||||
return Decoder::plain_text(body);
|
inner: Inner::Pending(Pending(
|
||||||
|
IoStream(body.into_stream()).peekable(),
|
||||||
|
DecoderType::Brotli,
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gzip")]
|
||||||
|
fn detect_gzip(headers: &mut HeaderMap) -> bool {
|
||||||
|
use http::header::{CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING};
|
||||||
|
use log::warn;
|
||||||
|
|
||||||
let content_encoding_gzip: bool;
|
let content_encoding_gzip: bool;
|
||||||
let mut is_gzip = {
|
let mut is_gzip = {
|
||||||
content_encoding_gzip = headers
|
content_encoding_gzip = headers
|
||||||
@@ -146,15 +134,76 @@ mod imp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if content_encoding_gzip {
|
if is_gzip {
|
||||||
headers.remove(CONTENT_ENCODING);
|
headers.remove(CONTENT_ENCODING);
|
||||||
headers.remove(CONTENT_LENGTH);
|
headers.remove(CONTENT_LENGTH);
|
||||||
}
|
}
|
||||||
if is_gzip {
|
is_gzip
|
||||||
Decoder::gzip(body)
|
}
|
||||||
} else {
|
|
||||||
Decoder::plain_text(body)
|
#[cfg(feature = "brotli")]
|
||||||
|
fn detect_brotli(headers: &mut HeaderMap) -> bool {
|
||||||
|
use http::header::{CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING};
|
||||||
|
use log::warn;
|
||||||
|
|
||||||
|
let content_encoding_gzip: bool;
|
||||||
|
let mut is_brotli = {
|
||||||
|
content_encoding_gzip = headers
|
||||||
|
.get_all(CONTENT_ENCODING)
|
||||||
|
.iter()
|
||||||
|
.any(|enc| enc == "br");
|
||||||
|
content_encoding_gzip
|
||||||
|
|| headers
|
||||||
|
.get_all(TRANSFER_ENCODING)
|
||||||
|
.iter()
|
||||||
|
.any(|enc| enc == "br")
|
||||||
|
};
|
||||||
|
if is_brotli {
|
||||||
|
if let Some(content_length) = headers.get(CONTENT_LENGTH) {
|
||||||
|
if content_length == "0" {
|
||||||
|
warn!("brotli response with content-length of 0");
|
||||||
|
is_brotli = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if is_brotli {
|
||||||
|
headers.remove(CONTENT_ENCODING);
|
||||||
|
headers.remove(CONTENT_LENGTH);
|
||||||
|
}
|
||||||
|
is_brotli
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a Decoder from a hyper request.
|
||||||
|
///
|
||||||
|
/// A decoder is just a wrapper around the hyper request that knows
|
||||||
|
/// how to decode the content body of the request.
|
||||||
|
///
|
||||||
|
/// Uses the correct variant by inspecting the Content-Encoding header.
|
||||||
|
pub(crate) fn detect(
|
||||||
|
_headers: &mut HeaderMap,
|
||||||
|
body: Body,
|
||||||
|
check_gzip: bool,
|
||||||
|
check_brotli: bool,
|
||||||
|
) -> Decoder {
|
||||||
|
if !check_gzip && !check_brotli {
|
||||||
|
return Decoder::plain_text(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gzip")]
|
||||||
|
{
|
||||||
|
if Decoder::detect_gzip(_headers) {
|
||||||
|
return Decoder::gzip(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "brotli")]
|
||||||
|
{
|
||||||
|
if Decoder::detect_brotli(_headers) {
|
||||||
|
return Decoder::brotli(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Decoder::plain_text(body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,26 +212,36 @@ mod imp {
|
|||||||
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||||
// Do a read or poll for a pending decoder value.
|
// Do a read or poll for a pending decoder value.
|
||||||
let new_value = match self.inner {
|
match self.inner {
|
||||||
|
#[cfg(any(feature = "brotli", feature = "gzip"))]
|
||||||
Inner::Pending(ref mut future) => match Pin::new(future).poll(cx) {
|
Inner::Pending(ref mut future) => match Pin::new(future).poll(cx) {
|
||||||
Poll::Ready(Ok(inner)) => inner,
|
Poll::Ready(Ok(inner)) => {
|
||||||
|
self.inner = inner;
|
||||||
|
return self.poll_next(cx);
|
||||||
|
}
|
||||||
Poll::Ready(Err(e)) => {
|
Poll::Ready(Err(e)) => {
|
||||||
return Poll::Ready(Some(Err(crate::error::decode_io(e))))
|
return Poll::Ready(Some(Err(crate::error::decode_io(e))));
|
||||||
}
|
}
|
||||||
Poll::Pending => return Poll::Pending,
|
Poll::Pending => return Poll::Pending,
|
||||||
},
|
},
|
||||||
Inner::PlainText(ref mut body) => return Pin::new(body).poll_next(cx),
|
Inner::PlainText(ref mut body) => return Pin::new(body).poll_next(cx),
|
||||||
|
#[cfg(feature = "gzip")]
|
||||||
Inner::Gzip(ref mut decoder) => {
|
Inner::Gzip(ref mut decoder) => {
|
||||||
return match futures_core::ready!(Pin::new(decoder).poll_next(cx)) {
|
return match futures_core::ready!(Pin::new(decoder).poll_next(cx)) {
|
||||||
Some(Ok(bytes)) => Poll::Ready(Some(Ok(bytes))),
|
Some(Ok(bytes)) => Poll::Ready(Some(Ok(bytes))),
|
||||||
Some(Err(err)) => Poll::Ready(Some(Err(crate::error::decode_io(err)))),
|
Some(Err(err)) => Poll::Ready(Some(Err(crate::error::decode_io(err)))),
|
||||||
None => Poll::Ready(None),
|
None => Poll::Ready(None),
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
#[cfg(feature = "brotli")]
|
||||||
|
Inner::Brotli(ref mut decoder) => {
|
||||||
|
return match futures_core::ready!(Pin::new(decoder).poll_next(cx)) {
|
||||||
|
Some(Ok(bytes)) => Poll::Ready(Some(Ok(bytes))),
|
||||||
|
Some(Err(err)) => Poll::Ready(Some(Err(crate::error::decode_io(err)))),
|
||||||
|
None => Poll::Ready(None),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.inner = new_value;
|
|
||||||
self.poll_next(cx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,11 +266,17 @@ mod imp {
|
|||||||
None => return Poll::Ready(Ok(Inner::PlainText(Body::empty().into_stream()))),
|
None => return Poll::Ready(Ok(Inner::PlainText(Body::empty().into_stream()))),
|
||||||
};
|
};
|
||||||
|
|
||||||
let body = mem::replace(
|
let _body = std::mem::replace(
|
||||||
&mut self.0,
|
&mut self.0,
|
||||||
IoStream(Body::empty().into_stream()).peekable(),
|
IoStream(Body::empty().into_stream()).peekable(),
|
||||||
);
|
);
|
||||||
Poll::Ready(Ok(Inner::Gzip(GzipDecoder::new(body))))
|
|
||||||
|
match self.1 {
|
||||||
|
#[cfg(feature = "brotli")]
|
||||||
|
DecoderType::Brotli => Poll::Ready(Ok(Inner::Brotli(BrotliDecoder::new(_body)))),
|
||||||
|
#[cfg(feature = "gzip")]
|
||||||
|
DecoderType::Gzip => Poll::Ready(Ok(Inner::Gzip(GzipDecoder::new(_body)))),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ impl Response {
|
|||||||
res: hyper::Response<hyper::Body>,
|
res: hyper::Response<hyper::Body>,
|
||||||
url: Url,
|
url: Url,
|
||||||
gzip: bool,
|
gzip: bool,
|
||||||
|
brotli: bool,
|
||||||
timeout: Option<Delay>,
|
timeout: Option<Delay>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let (parts, body) = res.into_parts();
|
let (parts, body) = res.into_parts();
|
||||||
@@ -47,7 +48,7 @@ impl Response {
|
|||||||
let extensions = parts.extensions;
|
let extensions = parts.extensions;
|
||||||
|
|
||||||
let mut headers = parts.headers;
|
let mut headers = parts.headers;
|
||||||
let decoder = Decoder::detect(&mut headers, Body::response(body, timeout), gzip);
|
let decoder = Decoder::detect(&mut headers, Body::response(body, timeout), gzip, brotli);
|
||||||
|
|
||||||
Response {
|
Response {
|
||||||
status,
|
status,
|
||||||
@@ -404,7 +405,7 @@ impl<T: Into<Body>> From<http::Response<T>> for Response {
|
|||||||
fn from(r: http::Response<T>) -> Response {
|
fn from(r: http::Response<T>) -> Response {
|
||||||
let (mut parts, body) = r.into_parts();
|
let (mut parts, body) = r.into_parts();
|
||||||
let body = body.into();
|
let body = body.into();
|
||||||
let body = Decoder::detect(&mut parts.headers, body, false);
|
let body = Decoder::detect(&mut parts.headers, body, false, false);
|
||||||
let url = parts
|
let url = parts
|
||||||
.extensions
|
.extensions
|
||||||
.remove::<ResponseUrl>()
|
.remove::<ResponseUrl>()
|
||||||
@@ -440,7 +441,6 @@ pub trait ResponseBuilderExt {
|
|||||||
fn url(self, url: Url) -> Self;
|
fn url(self, url: Url) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl ResponseBuilderExt for http::response::Builder {
|
impl ResponseBuilderExt for http::response::Builder {
|
||||||
fn url(self, url: Url) -> Self {
|
fn url(self, url: Url) -> Self {
|
||||||
self.extension(ResponseUrl(url))
|
self.extension(ResponseUrl(url))
|
||||||
|
|||||||
@@ -173,6 +173,7 @@
|
|||||||
//! - **blocking**: Provides the [blocking][] client API.
|
//! - **blocking**: Provides the [blocking][] client API.
|
||||||
//! - **cookies**: Provides cookie session support.
|
//! - **cookies**: Provides cookie session support.
|
||||||
//! - **gzip**: Provides response body gzip decompression.
|
//! - **gzip**: Provides response body gzip decompression.
|
||||||
|
//! - **brotli**: Provides response body brotli decompression.
|
||||||
//! - **json**: Provides serialization and deserialization for JSON bodies.
|
//! - **json**: Provides serialization and deserialization for JSON bodies.
|
||||||
//! - **stream**: Adds support for `futures::Stream`.
|
//! - **stream**: Adds support for `futures::Stream`.
|
||||||
//! - **socks**: Provides SOCKS5 proxy support.
|
//! - **socks**: Provides SOCKS5 proxy support.
|
||||||
|
|||||||
148
tests/brotli.rs
Normal file
148
tests/brotli.rs
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
mod support;
|
||||||
|
use std::io::Read;
|
||||||
|
use support::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn brotli_response() {
|
||||||
|
brotli_case(10_000, 4096).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn brotli_single_byte_chunks() {
|
||||||
|
brotli_case(10, 1).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_brotli_empty_body() {
|
||||||
|
let server = server::http(move |req| async move {
|
||||||
|
assert_eq!(req.method(), "HEAD");
|
||||||
|
|
||||||
|
http::Response::builder()
|
||||||
|
.header("content-encoding", "br")
|
||||||
|
.header("content-length", 100)
|
||||||
|
.body(Default::default())
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let res = client
|
||||||
|
.head(&format!("http://{}/brotli", server.addr()))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let body = res.text().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(body, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_accept_header_is_not_changed_if_set() {
|
||||||
|
let server = server::http(move |req| async move {
|
||||||
|
assert_eq!(req.headers()["accept"], "application/json");
|
||||||
|
assert!(req.headers()["accept-encoding"]
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.contains("br"));
|
||||||
|
http::Response::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let res = client
|
||||||
|
.get(&format!("http://{}/accept", server.addr()))
|
||||||
|
.header(
|
||||||
|
reqwest::header::ACCEPT,
|
||||||
|
reqwest::header::HeaderValue::from_static("application/json"),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(res.status(), reqwest::StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_accept_encoding_header_is_not_changed_if_set() {
|
||||||
|
let server = server::http(move |req| async move {
|
||||||
|
assert_eq!(req.headers()["accept"], "*/*");
|
||||||
|
assert_eq!(req.headers()["accept-encoding"], "identity");
|
||||||
|
http::Response::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let res = client
|
||||||
|
.get(&format!("http://{}/accept-encoding", server.addr()))
|
||||||
|
.header(
|
||||||
|
reqwest::header::ACCEPT_ENCODING,
|
||||||
|
reqwest::header::HeaderValue::from_static("identity"),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(res.status(), reqwest::StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn brotli_case(response_size: usize, chunk_size: usize) {
|
||||||
|
use futures_util::stream::StreamExt;
|
||||||
|
|
||||||
|
let content: String = (0..response_size)
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| format!("test {}", i))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut encoder = brotli_crate::CompressorReader::new(content.as_bytes(), 4096, 5, 20);
|
||||||
|
let mut brotlied_content = Vec::new();
|
||||||
|
encoder.read_to_end(&mut brotlied_content).unwrap();
|
||||||
|
|
||||||
|
let mut response = format!(
|
||||||
|
"\
|
||||||
|
HTTP/1.1 200 OK\r\n\
|
||||||
|
Server: test-accept\r\n\
|
||||||
|
Content-Encoding: br\r\n\
|
||||||
|
Content-Length: {}\r\n\
|
||||||
|
\r\n",
|
||||||
|
&brotlied_content.len()
|
||||||
|
)
|
||||||
|
.into_bytes();
|
||||||
|
response.extend(&brotlied_content);
|
||||||
|
|
||||||
|
let server = server::http(move |req| {
|
||||||
|
assert!(req.headers()["accept-encoding"]
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.contains("br"));
|
||||||
|
|
||||||
|
let brotlied = brotlied_content.clone();
|
||||||
|
async move {
|
||||||
|
let len = brotlied.len();
|
||||||
|
let stream =
|
||||||
|
futures_util::stream::unfold((brotlied, 0), move |(brotlied, pos)| async move {
|
||||||
|
let chunk = brotlied.chunks(chunk_size).nth(pos)?.to_vec();
|
||||||
|
|
||||||
|
Some((chunk, (brotlied, pos + 1)))
|
||||||
|
});
|
||||||
|
|
||||||
|
let body = hyper::Body::wrap_stream(stream.map(Ok::<_, std::convert::Infallible>));
|
||||||
|
|
||||||
|
http::Response::builder()
|
||||||
|
.header("content-encoding", "br")
|
||||||
|
.header("content-length", len)
|
||||||
|
.body(body)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let res = client
|
||||||
|
.get(&format!("http://{}/brotli", server.addr()))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("response");
|
||||||
|
|
||||||
|
let body = res.text().await.expect("text");
|
||||||
|
assert_eq!(body, content);
|
||||||
|
}
|
||||||
@@ -12,7 +12,16 @@ async fn auto_headers() {
|
|||||||
assert_eq!(req.headers()["accept"], "*/*");
|
assert_eq!(req.headers()["accept"], "*/*");
|
||||||
assert_eq!(req.headers().get("user-agent"), None);
|
assert_eq!(req.headers().get("user-agent"), None);
|
||||||
if cfg!(feature = "gzip") {
|
if cfg!(feature = "gzip") {
|
||||||
assert_eq!(req.headers()["accept-encoding"], "gzip");
|
assert!(req.headers()["accept-encoding"]
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.contains("gzip"));
|
||||||
|
}
|
||||||
|
if cfg!(feature = "brotli") {
|
||||||
|
assert!(req.headers()["accept-encoding"]
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.contains("br"));
|
||||||
}
|
}
|
||||||
|
|
||||||
http::Response::default()
|
http::Response::default()
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ async fn test_gzip_empty_body() {
|
|||||||
async fn test_accept_header_is_not_changed_if_set() {
|
async fn test_accept_header_is_not_changed_if_set() {
|
||||||
let server = server::http(move |req| async move {
|
let server = server::http(move |req| async move {
|
||||||
assert_eq!(req.headers()["accept"], "application/json");
|
assert_eq!(req.headers()["accept"], "application/json");
|
||||||
assert_eq!(req.headers()["accept-encoding"], "gzip");
|
assert!(req.headers()["accept-encoding"]
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.contains("gzip"));
|
||||||
http::Response::default()
|
http::Response::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,7 +114,10 @@ async fn gzip_case(response_size: usize, chunk_size: usize) {
|
|||||||
response.extend(&gzipped_content);
|
response.extend(&gzipped_content);
|
||||||
|
|
||||||
let server = server::http(move |req| {
|
let server = server::http(move |req| {
|
||||||
assert_eq!(req.headers()["accept-encoding"], "gzip");
|
assert!(req.headers()["accept-encoding"]
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.contains("gzip"));
|
||||||
|
|
||||||
let gzipped = gzipped_content.clone();
|
let gzipped = gzipped_content.clone();
|
||||||
async move {
|
async move {
|
||||||
|
|||||||
Reference in New Issue
Block a user