Support Deflate decoding (#1250)

This commit is contained in:
Mohamed Daahir
2021-04-22 18:35:29 +01:00
committed by GitHub
parent 42b31600c3
commit 77ee0df7c5
8 changed files with 328 additions and 57 deletions

View File

@@ -536,6 +536,29 @@ impl ClientBuilder {
self
}
/// Enable auto deflate decompression by checking the `Content-Encoding` response header.
///
/// If auto deflate 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 `deflate`.
/// The request body is **not** automatically compressed.
/// - When receiving a response, if it's headers contain a `Content-Encoding` value that
/// equals to `deflate`, both values `Content-Encoding` and `Content-Length` are removed from the
/// headers' set. The response body is automatically decompressed.
///
/// If the `deflate` feature is turned on, the default option is enabled.
///
/// # Optional
///
/// This requires the optional `deflate` feature to be enabled
#[cfg(feature = "deflate")]
#[cfg_attr(docsrs, doc(cfg(feature = "deflate")))]
pub fn deflate(mut self, enable: bool) -> ClientBuilder {
self.config.accepts.deflate = enable;
self
}
/// Disable auto response body gzip decompression.
///
/// This method exists even if the optional `gzip` feature is not enabled.
@@ -570,6 +593,23 @@ impl ClientBuilder {
}
}
/// Disable auto response body deflate decompression.
///
/// This method exists even if the optional `deflate` feature is not enabled.
/// This can be used to ensure a `Client` doesn't use deflate decompression
/// even if another dependency were to enable the optional `deflate` feature.
pub fn no_deflate(self) -> ClientBuilder {
#[cfg(feature = "deflate")]
{
self.deflate(false)
}
#[cfg(not(feature = "deflate"))]
{
self
}
}
// Redirect options
/// Set a `RedirectPolicy` for this client.

View File

@@ -9,15 +9,18 @@ use async_compression::tokio::bufread::GzipDecoder;
#[cfg(feature = "brotli")]
use async_compression::tokio::bufread::BrotliDecoder;
#[cfg(feature = "deflate")]
use async_compression::tokio::bufread::DeflateDecoder;
use bytes::Bytes;
use futures_core::Stream;
use futures_util::stream::Peekable;
use http::HeaderMap;
use hyper::body::HttpBody;
#[cfg(any(feature = "gzip", feature = "brotli"))]
#[cfg(any(feature = "gzip", feature = "brotli", feature = "deflate"))]
use tokio_util::codec::{BytesCodec, FramedRead};
#[cfg(any(feature = "gzip", feature = "brotli"))]
#[cfg(any(feature = "gzip", feature = "brotli", feature = "deflate"))]
use tokio_util::io::StreamReader;
use super::super::Body;
@@ -29,6 +32,8 @@ pub(super) struct Accepts {
pub(super) gzip: bool,
#[cfg(feature = "brotli")]
pub(super) brotli: bool,
#[cfg(feature = "deflate")]
pub(super) deflate: bool,
}
/// A response decompressor over a non-blocking stream of chunks.
@@ -50,8 +55,12 @@ enum Inner {
#[cfg(feature = "brotli")]
Brotli(FramedRead<BrotliDecoder<StreamReader<Peekable<IoStream>, Bytes>>, BytesCodec>),
/// A `Deflate` decoder will uncompress the deflated response content before returning it.
#[cfg(feature = "deflate")]
Deflate(FramedRead<DeflateDecoder<StreamReader<Peekable<IoStream>, Bytes>>, BytesCodec>),
/// A decoder that doesn't have a value yet.
#[cfg(any(feature = "brotli", feature = "gzip"))]
#[cfg(any(feature = "brotli", feature = "gzip", feature = "deflate"))]
Pending(Pending),
}
@@ -65,6 +74,8 @@ enum DecoderType {
Gzip,
#[cfg(feature = "brotli")]
Brotli,
#[cfg(feature = "deflate")]
Deflate,
}
impl fmt::Debug for Decoder {
@@ -120,68 +131,49 @@ impl Decoder {
}
}
#[cfg(feature = "gzip")]
fn detect_gzip(headers: &mut HeaderMap) -> bool {
use http::header::{CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING};
use log::warn;
/// A deflate decoder.
///
/// This decoder will buffer and decompress chunks that are deflated.
#[cfg(feature = "deflate")]
fn deflate(body: Body) -> Decoder {
use futures_util::StreamExt;
let content_encoding_gzip: bool;
let mut is_gzip = {
content_encoding_gzip = headers
.get_all(CONTENT_ENCODING)
.iter()
.any(|enc| enc == "gzip");
content_encoding_gzip
|| headers
.get_all(TRANSFER_ENCODING)
.iter()
.any(|enc| enc == "gzip")
};
if is_gzip {
if let Some(content_length) = headers.get(CONTENT_LENGTH) {
if content_length == "0" {
warn!("gzip response with content-length of 0");
is_gzip = false;
}
}
Decoder {
inner: Inner::Pending(Pending(
IoStream(body.into_stream()).peekable(),
DecoderType::Deflate,
)),
}
if is_gzip {
headers.remove(CONTENT_ENCODING);
headers.remove(CONTENT_LENGTH);
}
is_gzip
}
#[cfg(feature = "brotli")]
fn detect_brotli(headers: &mut HeaderMap) -> bool {
#[cfg(any(feature = "brotli", feature = "gzip", feature = "deflate"))]
fn detect_encoding(headers: &mut HeaderMap, encoding_str: &str) -> 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
let mut is_content_encoded = {
headers
.get_all(CONTENT_ENCODING)
.iter()
.any(|enc| enc == "br");
content_encoding_gzip
.any(|enc| enc == encoding_str)
|| headers
.get_all(TRANSFER_ENCODING)
.iter()
.any(|enc| enc == "br")
.any(|enc| enc == encoding_str)
};
if is_brotli {
if is_content_encoded {
if let Some(content_length) = headers.get(CONTENT_LENGTH) {
if content_length == "0" {
warn!("brotli response with content-length of 0");
is_brotli = false;
warn!("{} response with content-length of 0", encoding_str);
is_content_encoded = false;
}
}
}
if is_brotli {
if is_content_encoded {
headers.remove(CONTENT_ENCODING);
headers.remove(CONTENT_LENGTH);
}
is_brotli
is_content_encoded
}
/// Constructs a Decoder from a hyper request.
@@ -193,18 +185,25 @@ impl Decoder {
pub(super) fn detect(_headers: &mut HeaderMap, body: Body, _accepts: Accepts) -> Decoder {
#[cfg(feature = "gzip")]
{
if _accepts.gzip && Decoder::detect_gzip(_headers) {
if _accepts.gzip && Decoder::detect_encoding(_headers, "gzip") {
return Decoder::gzip(body);
}
}
#[cfg(feature = "brotli")]
{
if _accepts.brotli && Decoder::detect_brotli(_headers) {
if _accepts.brotli && Decoder::detect_encoding(_headers, "br") {
return Decoder::brotli(body);
}
}
#[cfg(feature = "deflate")]
{
if _accepts.deflate && Decoder::detect_encoding(_headers, "deflate") {
return Decoder::deflate(body);
}
}
Decoder::plain_text(body)
}
}
@@ -215,7 +214,7 @@ impl Stream for Decoder {
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.
match self.inner {
#[cfg(any(feature = "brotli", feature = "gzip"))]
#[cfg(any(feature = "brotli", feature = "gzip", feature = "deflate"))]
Inner::Pending(ref mut future) => match Pin::new(future).poll(cx) {
Poll::Ready(Ok(inner)) => {
self.inner = inner;
@@ -243,6 +242,14 @@ impl Stream for Decoder {
None => Poll::Ready(None),
};
}
#[cfg(feature = "deflate")]
Inner::Deflate(ref mut decoder) => {
return match futures_core::ready!(Pin::new(decoder).poll_next(cx)) {
Some(Ok(bytes)) => Poll::Ready(Some(Ok(bytes.freeze()))),
Some(Err(err)) => Poll::Ready(Some(Err(crate::error::decode_io(err)))),
None => Poll::Ready(None),
};
}
}
}
}
@@ -269,7 +276,7 @@ impl HttpBody for Decoder {
match self.inner {
Inner::PlainText(ref body) => HttpBody::size_hint(body),
// the rest are "unknown", so default
#[cfg(any(feature = "brotli", feature = "gzip"))]
#[cfg(any(feature = "brotli", feature = "gzip", feature = "deflate"))]
_ => http_body::SizeHint::default(),
}
}
@@ -312,6 +319,11 @@ impl Future for Pending {
GzipDecoder::new(StreamReader::new(_body)),
BytesCodec::new(),
)))),
#[cfg(feature = "deflate")]
DecoderType::Deflate => Poll::Ready(Ok(Inner::Deflate(FramedRead::new(
DeflateDecoder::new(StreamReader::new(_body)),
BytesCodec::new(),
)))),
}
}
}
@@ -337,15 +349,21 @@ impl Accepts {
gzip: false,
#[cfg(feature = "brotli")]
brotli: false,
#[cfg(feature = "deflate")]
deflate: false,
}
}
pub(super) fn as_str(&self) -> Option<&'static str> {
match (self.is_gzip(), self.is_brotli()) {
(true, true) => Some("gzip, br"),
(true, false) => Some("gzip"),
(false, true) => Some("br"),
_ => None,
match (self.is_gzip(), self.is_brotli(), self.is_deflate()) {
(true, true, true) => Some("gzip, br, deflate"),
(true, true, false) => Some("gzip, br"),
(true, false, true) => Some("gzip, deflate"),
(false, true, true) => Some("br, deflate"),
(true, false, false) => Some("gzip"),
(false, true, false) => Some("br"),
(false, false, true) => Some("deflate"),
(false, false, false) => None,
}
}
@@ -372,6 +390,18 @@ impl Accepts {
false
}
}
fn is_deflate(&self) -> bool {
#[cfg(feature = "deflate")]
{
self.deflate
}
#[cfg(not(feature = "deflate"))]
{
false
}
}
}
impl Default for Accepts {
@@ -381,6 +411,8 @@ impl Default for Accepts {
gzip: true,
#[cfg(feature = "brotli")]
brotli: true,
#[cfg(feature = "deflate")]
deflate: true,
}
}
}

View File

@@ -257,6 +257,28 @@ impl ClientBuilder {
self.with_inner(|inner| inner.brotli(enable))
}
/// Enable auto deflate decompression by checking the `Content-Encoding` response header.
///
/// If auto deflate decompresson 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 `deflate`.
/// The request body is **not** automatically compressed.
/// - When receiving a response, if it's headers contain a `Content-Encoding` value that
/// equals to `deflate`, both values `Content-Encoding` and `Content-Length` are removed from the
/// headers' set. The response body is automatically decompressed.
///
/// If the `deflate` feature is turned on, the default option is enabled.
///
/// # Optional
///
/// This requires the optional `deflate` feature to be enabled
#[cfg(feature = "deflate")]
#[cfg_attr(docsrs, doc(cfg(feature = "deflate")))]
pub fn deflate(self, enable: bool) -> ClientBuilder {
self.with_inner(|inner| inner.deflate(enable))
}
/// Disable auto response body gzip decompression.
///
/// This method exists even if the optional `gzip` feature is not enabled.
@@ -275,6 +297,15 @@ impl ClientBuilder {
self.with_inner(|inner| inner.no_brotli())
}
/// Disable auto response body deflate decompression.
///
/// This method exists even if the optional `deflate` feature is not enabled.
/// This can be used to ensure a `Client` doesn't use deflate decompression
/// even if another dependency were to enable the optional `deflate` feature.
pub fn no_deflate(self) -> ClientBuilder {
self.with_inner(|inner| inner.no_deflate())
}
// Redirect options
/// Set a `redirect::Policy` for this client.

View File

@@ -182,6 +182,7 @@
//! - **cookies**: Provides cookie session support.
//! - **gzip**: Provides response body gzip decompression.
//! - **brotli**: Provides response body brotli decompression.
//! - **deflate**: Provides response body deflate decompression.
//! - **json**: Provides serialization and deserialization for JSON bodies.
//! - **multipart**: Provides functionality for multipart forms.
//! - **stream**: Adds support for `futures::Stream`.