Add support for gzip and brotli
Automatically sends the Accept-Encoding header on requests. Not runtime-configurable, only with Cargo features.
This commit is contained in:
committed by
Martin Algesten
parent
0f0dec5f32
commit
873e6066f3
@@ -22,6 +22,8 @@ json = ["serde", "serde_json"]
|
|||||||
charset = ["encoding_rs"]
|
charset = ["encoding_rs"]
|
||||||
cookies = ["cookie", "cookie_store"]
|
cookies = ["cookie", "cookie_store"]
|
||||||
socks-proxy = ["socks"]
|
socks-proxy = ["socks"]
|
||||||
|
gzip = ["flate2"]
|
||||||
|
brotli = ["brotli-decompressor"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
@@ -40,6 +42,8 @@ webpki-roots = { version = "0.22", optional = true }
|
|||||||
rustls = { version = "0.20", optional = true }
|
rustls = { version = "0.20", optional = true }
|
||||||
rustls-native-certs = { version = "0.6", optional = true }
|
rustls-native-certs = { version = "0.6", optional = true }
|
||||||
native-tls = { version = "0.2", optional = true }
|
native-tls = { version = "0.2", optional = true }
|
||||||
|
flate2 = { version = "1.0.22", optional = true }
|
||||||
|
brotli-decompressor = { version = "2.3.2", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|||||||
@@ -87,12 +87,36 @@ impl Request {
|
|||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_call(self, payload: Payload) -> Result<Response> {
|
/// Add Accept-Encoding header with supported values, unless user has
|
||||||
|
/// already set this header or is requesting a specific byte-range.
|
||||||
|
#[cfg(any(feature = "gzip", feature = "brotli"))]
|
||||||
|
fn add_accept_encoding(&mut self) {
|
||||||
|
let should_add = !self.headers.iter().map(|h| h.name()).any(|name| {
|
||||||
|
name.eq_ignore_ascii_case("accept-encoding") || name.eq_ignore_ascii_case("range")
|
||||||
|
});
|
||||||
|
if should_add {
|
||||||
|
const GZ: bool = cfg!(feature = "gzip");
|
||||||
|
const BR: bool = cfg!(feature = "brotli");
|
||||||
|
const ACCEPT: &str = match (GZ, BR) {
|
||||||
|
(true, true) => "gzip, br",
|
||||||
|
(true, false) => "gzip",
|
||||||
|
(false, true) => "br",
|
||||||
|
(false, false) => "identity", // unreachable due to cfg feature on this fn
|
||||||
|
};
|
||||||
|
self.headers.push(Header::new("accept-encoding", ACCEPT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(not(any(feature = "gzip", feature = "brotli")), allow(unused_mut))]
|
||||||
|
fn do_call(mut self, payload: Payload) -> Result<Response> {
|
||||||
for h in &self.headers {
|
for h in &self.headers {
|
||||||
h.validate()?;
|
h.validate()?;
|
||||||
}
|
}
|
||||||
let url = self.parse_url()?;
|
let url = self.parse_url()?;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "gzip", feature = "brotli"))]
|
||||||
|
self.add_accept_encoding();
|
||||||
|
|
||||||
let deadline = match self.timeout.or(self.agent.config.timeout) {
|
let deadline = match self.timeout.or(self.agent.config.timeout) {
|
||||||
None => None,
|
None => None,
|
||||||
Some(timeout) => {
|
Some(timeout) => {
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ use serde::de::DeserializeOwned;
|
|||||||
#[cfg(feature = "charset")]
|
#[cfg(feature = "charset")]
|
||||||
use encoding_rs::Encoding;
|
use encoding_rs::Encoding;
|
||||||
|
|
||||||
|
#[cfg(feature = "gzip")]
|
||||||
|
use flate2::read::GzDecoder;
|
||||||
|
|
||||||
|
#[cfg(feature = "brotli")]
|
||||||
|
use brotli_decompressor::Decompressor;
|
||||||
|
|
||||||
pub const DEFAULT_CONTENT_TYPE: &str = "text/plain";
|
pub const DEFAULT_CONTENT_TYPE: &str = "text/plain";
|
||||||
pub const DEFAULT_CHARACTER_SET: &str = "utf-8";
|
pub const DEFAULT_CHARACTER_SET: &str = "utf-8";
|
||||||
const INTO_STRING_LIMIT: usize = 10 * 1_024 * 1_024;
|
const INTO_STRING_LIMIT: usize = 10 * 1_024 * 1_024;
|
||||||
@@ -281,6 +287,11 @@ impl Response {
|
|||||||
.and_then(|l| l.parse::<usize>().ok())
|
.and_then(|l| l.parse::<usize>().ok())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let compression = self
|
||||||
|
.header("content-encoding")
|
||||||
|
.map(Compression::from_header_value)
|
||||||
|
.flatten();
|
||||||
|
|
||||||
let stream = self.stream;
|
let stream = self.stream;
|
||||||
let unit = self.unit;
|
let unit = self.unit;
|
||||||
if let Some(unit) = &unit {
|
if let Some(unit) = &unit {
|
||||||
@@ -292,12 +303,17 @@ impl Response {
|
|||||||
let deadline = unit.as_ref().and_then(|u| u.deadline);
|
let deadline = unit.as_ref().and_then(|u| u.deadline);
|
||||||
let stream = DeadlineStream::new(*stream, deadline);
|
let stream = DeadlineStream::new(*stream, deadline);
|
||||||
|
|
||||||
match (use_chunked, limit_bytes) {
|
let body_reader: Box<dyn Read + Send> = match (use_chunked, limit_bytes) {
|
||||||
(true, _) => Box::new(PoolReturnRead::new(unit, ChunkDecoder::new(stream))),
|
(true, _) => Box::new(PoolReturnRead::new(unit, ChunkDecoder::new(stream))),
|
||||||
(false, Some(len)) => {
|
(false, Some(len)) => {
|
||||||
Box::new(PoolReturnRead::new(unit, LimitedRead::new(stream, len)))
|
Box::new(PoolReturnRead::new(unit, LimitedRead::new(stream, len)))
|
||||||
}
|
}
|
||||||
(false, None) => Box::new(stream),
|
(false, None) => Box::new(stream),
|
||||||
|
};
|
||||||
|
|
||||||
|
match compression {
|
||||||
|
None => body_reader,
|
||||||
|
Some(c) => c.wrap_reader(body_reader),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,6 +524,38 @@ impl Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
enum Compression {
|
||||||
|
#[cfg(feature = "brotli")]
|
||||||
|
Brotli,
|
||||||
|
#[cfg(feature = "gzip")]
|
||||||
|
Gzip,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Compression {
|
||||||
|
/// Convert a string like "br" to an enum value
|
||||||
|
fn from_header_value(value: &str) -> Option<Compression> {
|
||||||
|
match value {
|
||||||
|
#[cfg(feature = "brotli")]
|
||||||
|
"br" => Some(Compression::Brotli),
|
||||||
|
#[cfg(feature = "gzip")]
|
||||||
|
"gzip" | "x-gzip" => Some(Compression::Gzip),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrap the raw reader with a decompressing reader
|
||||||
|
#[allow(unused_variables)] // when no features enabled, reader is unused (unreachable)
|
||||||
|
fn wrap_reader(self, reader: Box<dyn Read + Send>) -> Box<dyn Read + Send> {
|
||||||
|
match self {
|
||||||
|
#[cfg(feature = "brotli")]
|
||||||
|
Compression::Brotli => Box::new(Decompressor::new(reader, 4096)),
|
||||||
|
#[cfg(feature = "gzip")]
|
||||||
|
Compression::Gzip => Box::new(GzDecoder::new(reader)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// parse a line like: HTTP/1.1 200 OK\r\n
|
/// parse a line like: HTTP/1.1 200 OK\r\n
|
||||||
fn parse_status_line(line: &str) -> Result<(ResponseStatusIndex, u16), Error> {
|
fn parse_status_line(line: &str) -> Result<(ResponseStatusIndex, u16), Error> {
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in New Issue
Block a user