Support Deflate decoding (#1250)
This commit is contained in:
		| @@ -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. | ||||
|   | ||||
| @@ -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, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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`. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user