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:
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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`]
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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.0–1.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");
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user