1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-06-25 06:39:22 +02:00

Fix AcceptEncoding header (#2501)

This commit is contained in:
Rob Ede
2022-01-03 13:17:57 +00:00
committed by GitHub
parent b708924590
commit e890307091
33 changed files with 1360 additions and 889 deletions

View File

@ -47,9 +47,9 @@ where
pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
let decoder = match encoding {
#[cfg(feature = "compress-brotli")]
ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(BrotliDecoder::new(
Writer::new(),
)))),
ContentEncoding::Brotli => Some(ContentDecoder::Brotli(Box::new(
BrotliDecoder::new(Writer::new()),
))),
#[cfg(feature = "compress-gzip")]
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
ZlibDecoder::new(Writer::new()),
@ -165,7 +165,7 @@ enum ContentDecoder {
#[cfg(feature = "compress-gzip")]
Gzip(Box<GzDecoder<Writer>>),
#[cfg(feature = "compress-brotli")]
Br(Box<BrotliDecoder<Writer>>),
Brotli(Box<BrotliDecoder<Writer>>),
// We need explicit 'static lifetime here because ZstdDecoder need lifetime
// argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static`
#[cfg(feature = "compress-zstd")]
@ -176,7 +176,7 @@ impl ContentDecoder {
fn feed_eof(&mut self) -> io::Result<Option<Bytes>> {
match self {
#[cfg(feature = "compress-brotli")]
ContentDecoder::Br(ref mut decoder) => match decoder.flush() {
ContentDecoder::Brotli(ref mut decoder) => match decoder.flush() {
Ok(()) => {
let b = decoder.get_mut().take();
@ -234,7 +234,7 @@ impl ContentDecoder {
fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> {
match self {
#[cfg(feature = "compress-brotli")]
ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) {
ContentDecoder::Brotli(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => {
decoder.flush()?;
let b = decoder.get_mut().take();

View File

@ -56,11 +56,10 @@ impl<B: MessageBody> Encoder<B> {
}
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self {
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
let should_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|| head.status == StatusCode::SWITCHING_PROTOCOLS
|| head.status == StatusCode::NO_CONTENT
|| encoding == ContentEncoding::Identity
|| encoding == ContentEncoding::Auto);
|| encoding == ContentEncoding::Identity);
// no need to compress an empty body
if matches!(body.size(), BodySize::None) {
@ -72,8 +71,8 @@ impl<B: MessageBody> Encoder<B> {
Err(body) => EncoderBody::Stream { body },
};
if can_encode {
// Modify response body only if encoder is set
if should_encode {
// wrap body only if encoder is feature-enabled
if let Some(enc) = ContentEncoder::encoder(encoding) {
update_head(encoding, head);
@ -252,10 +251,10 @@ where
}
fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
head.headers_mut().insert(
header::CONTENT_ENCODING,
HeaderValue::from_static(encoding.as_str()),
);
head.headers_mut()
.insert(header::CONTENT_ENCODING, encoding.to_header_value());
head.headers_mut()
.insert(header::VARY, HeaderValue::from_static("accept-encoding"));
head.no_chunking(false);
}
@ -268,7 +267,7 @@ enum ContentEncoder {
Gzip(GzEncoder<Writer>),
#[cfg(feature = "compress-brotli")]
Br(BrotliEncoder<Writer>),
Brotli(BrotliEncoder<Writer>),
// Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we
// use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
@ -292,8 +291,8 @@ impl ContentEncoder {
))),
#[cfg(feature = "compress-brotli")]
ContentEncoding::Br => {
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
ContentEncoding::Brotli => {
Some(ContentEncoder::Brotli(BrotliEncoder::new(Writer::new(), 3)))
}
#[cfg(feature = "compress-zstd")]
@ -302,7 +301,7 @@ impl ContentEncoder {
Some(ContentEncoder::Zstd(encoder))
}
_ => None,
ContentEncoding::Identity => None,
}
}
@ -310,7 +309,7 @@ impl ContentEncoder {
pub(crate) fn take(&mut self) -> Bytes {
match *self {
#[cfg(feature = "compress-brotli")]
ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(),
ContentEncoder::Brotli(ref mut encoder) => encoder.get_mut().take(),
#[cfg(feature = "compress-gzip")]
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
@ -326,7 +325,7 @@ impl ContentEncoder {
fn finish(self) -> Result<Bytes, io::Error> {
match self {
#[cfg(feature = "compress-brotli")]
ContentEncoder::Br(encoder) => match encoder.finish() {
ContentEncoder::Brotli(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err),
},
@ -354,7 +353,7 @@ impl ContentEncoder {
fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
match *self {
#[cfg(feature = "compress-brotli")]
ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) {
ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()),
Err(err) => {
trace!("Error decoding br encoding: {}", err);

View File

@ -250,6 +250,7 @@ impl From<ParseError> for Response<BoxBody> {
/// A set of errors that can occur running blocking tasks in thread pool.
#[derive(Debug, Display, Error)]
#[display(fmt = "Blocking thread pool is gone")]
// TODO: non-exhaustive
pub struct BlockingError;
/// A set of errors that can occur during payload parsing.

View File

@ -605,6 +605,13 @@ impl<'a> IntoIterator for &'a HeaderMap {
}
}
/// Convert `http::HeaderMap` to our `HeaderMap`.
impl From<http::HeaderMap> for HeaderMap {
fn from(mut map: http::HeaderMap) -> HeaderMap {
HeaderMap::from_drain(map.drain())
}
}
/// Iterator over removed, owned values with the same associated name.
///
/// Returned from methods that remove or replace items. See [`HeaderMap::insert`]

View File

@ -57,13 +57,6 @@ pub trait Header: TryIntoHeaderValue {
fn parse<M: HttpMessage>(msg: &M) -> Result<Self, ParseError>;
}
/// Convert `http::HeaderMap` to our `HeaderMap`.
impl From<http::HeaderMap> for HeaderMap {
fn from(mut map: http::HeaderMap) -> HeaderMap {
HeaderMap::from_drain(map.drain())
}
}
/// This encode set is used for HTTP header values and is defined at
/// <https://datatracker.ietf.org/doc/html/rfc5987#section-3.2>.
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS

View File

@ -20,14 +20,16 @@ pub struct ContentEncodingParseError;
/// See [IANA HTTP Content Coding Registry].
///
/// [IANA HTTP Content Coding Registry]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ContentEncoding {
/// Automatically select encoding based on encoding negotiation.
Auto,
/// Indicates the no-op identity encoding.
///
/// I.e., no compression or modification.
Identity,
/// A format using the Brotli algorithm.
Br,
Brotli,
/// A format using the zlib structure with deflate algorithm.
Deflate,
@ -37,27 +39,30 @@ pub enum ContentEncoding {
/// Zstd algorithm.
Zstd,
/// Indicates the identity function (i.e. no compression, nor modification).
Identity,
}
impl ContentEncoding {
/// Is the content compressed?
#[inline]
pub const fn is_compression(self) -> bool {
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
}
/// Convert content encoding to string.
#[inline]
pub const fn as_str(self) -> &'static str {
match self {
ContentEncoding::Br => "br",
ContentEncoding::Brotli => "br",
ContentEncoding::Gzip => "gzip",
ContentEncoding::Deflate => "deflate",
ContentEncoding::Zstd => "zstd",
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
ContentEncoding::Identity => "identity",
}
}
/// Convert content encoding to header value.
#[inline]
pub const fn to_header_value(self) -> HeaderValue {
match self {
ContentEncoding::Brotli => HeaderValue::from_static("br"),
ContentEncoding::Gzip => HeaderValue::from_static("gzip"),
ContentEncoding::Deflate => HeaderValue::from_static("deflate"),
ContentEncoding::Zstd => HeaderValue::from_static("zstd"),
ContentEncoding::Identity => HeaderValue::from_static("identity"),
}
}
}
@ -71,16 +76,18 @@ impl Default for ContentEncoding {
impl FromStr for ContentEncoding {
type Err = ContentEncodingParseError;
fn from_str(val: &str) -> Result<Self, Self::Err> {
let val = val.trim();
fn from_str(enc: &str) -> Result<Self, Self::Err> {
let enc = enc.trim();
if val.eq_ignore_ascii_case("br") {
Ok(ContentEncoding::Br)
} else if val.eq_ignore_ascii_case("gzip") {
if enc.eq_ignore_ascii_case("br") {
Ok(ContentEncoding::Brotli)
} else if enc.eq_ignore_ascii_case("gzip") {
Ok(ContentEncoding::Gzip)
} else if val.eq_ignore_ascii_case("deflate") {
} else if enc.eq_ignore_ascii_case("deflate") {
Ok(ContentEncoding::Deflate)
} else if val.eq_ignore_ascii_case("zstd") {
} else if enc.eq_ignore_ascii_case("identity") {
Ok(ContentEncoding::Identity)
} else if enc.eq_ignore_ascii_case("zstd") {
Ok(ContentEncoding::Zstd)
} else {
Err(ContentEncodingParseError)

View File

@ -27,7 +27,8 @@ const MAX_QUALITY_FLOAT: f32 = 1.0;
///
/// assert_eq!(q(0.42).to_string(), "0.42");
/// assert_eq!(q(1.0).to_string(), "1");
/// assert_eq!(Quality::MIN.to_string(), "0");
/// assert_eq!(Quality::MIN.to_string(), "0.001");
/// assert_eq!(Quality::ZERO.to_string(), "0");
/// ```
///
/// [RFC 7231 §5.3.1]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1
@ -38,8 +39,11 @@ impl Quality {
/// The maximum quality value, equivalent to `q=1.0`.
pub const MAX: Quality = Quality(MAX_QUALITY_INT);
/// The minimum quality value, equivalent to `q=0.0`.
pub const MIN: Quality = Quality(0);
/// The minimum, non-zero quality value, equivalent to `q=0.001`.
pub const MIN: Quality = Quality(1);
/// The zero quality value, equivalent to `q=0.0`.
pub const ZERO: Quality = Quality(0);
/// Converts a float in the range 0.01.0 to a `Quality`.
///
@ -51,7 +55,7 @@ impl Quality {
// Check that `value` is within range should be done before calling this method.
// Just in case, this debug_assert should catch if we were forgetful.
debug_assert!(
(0.0f32..=1.0f32).contains(&value),
(0.0..=MAX_QUALITY_FLOAT).contains(&value),
"q value must be between 0.0 and 1.0"
);
@ -154,10 +158,13 @@ impl TryFrom<f32> for Quality {
/// let q1 = q(1.0);
/// assert_eq!(q1, Quality::MAX);
///
/// let q2 = q(0.0);
/// let q2 = q(0.001);
/// assert_eq!(q2, Quality::MIN);
///
/// let q3 = q(0.42);
/// let q3 = q(0.0);
/// assert_eq!(q3, Quality::ZERO);
///
/// let q4 = q(0.42);
/// ```
///
/// An out-of-range `f32` quality will panic.
@ -185,6 +192,10 @@ mod tests {
#[test]
fn display_output() {
assert_eq!(Quality::ZERO.to_string(), "0");
assert_eq!(Quality::MIN.to_string(), "0.001");
assert_eq!(Quality::MAX.to_string(), "1");
assert_eq!(q(0.0).to_string(), "0");
assert_eq!(q(1.0).to_string(), "1");
assert_eq!(q(0.001).to_string(), "0.001");

View File

@ -31,7 +31,7 @@ use super::Quality;
/// let q_item_fallback: QualityItem<String> = "abc;q=0.1".parse().unwrap();
/// assert!(q_item > q_item_fallback);
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QualityItem<T> {
/// The wrapped contents of the field.
pub item: T,
@ -53,10 +53,15 @@ impl<T> QualityItem<T> {
Self::new(item, Quality::MAX)
}
/// Constructs a new `QualityItem` from an item, using the minimum q-value.
/// Constructs a new `QualityItem` from an item, using the minimum, non-zero q-value.
pub fn min(item: T) -> Self {
Self::new(item, Quality::MIN)
}
/// Constructs a new `QualityItem` from an item, using zero q-value of zero.
pub fn zero(item: T) -> Self {
Self::new(item, Quality::ZERO)
}
}
impl<T: PartialEq> PartialOrd for QualityItem<T> {
@ -73,7 +78,10 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
// q-factor value is implied for max value
Quality::MAX => Ok(()),
Quality::MIN => f.write_str("; q=0"),
// fast path for zero
Quality::ZERO => f.write_str("; q=0"),
// quality formatting is already using itoa
q => write!(f, "; q={}", q),
}
}