1
0
Fork 0
mirror of https://github.com/actix/actix-web.git synced 2024-06-11 17:59:35 +00: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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1360 additions and 889 deletions

View file

@ -1,8 +1,6 @@
name: Benchmark name: Benchmark
on: on:
pull_request:
types: [opened, synchronize, reopened]
push: push:
branches: branches:
- master - master

View file

@ -1,6 +1,22 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
### Added
- `impl Hash` for `http::header::Encoding`. [#2501]
- `AcceptEncoding::negotiate()`. [#2501]
### Changed
- `AcceptEncoding::preference` now returns `Option<Preference<Encoding>>`. [#2501]
- Rename methods `BodyEncoding::{encoding => encode_with, get_encoding => preferred_encoding}`. [#2501]
- `http::header::Encoding` now only represents `Content-Encoding` types. [#2501]
### Fixed
- Auto-negotiation of content encoding is more fault-tolerant when using the `Compress` middleware. [#2501]
### Removed
- `Compress::new`; restricting compression algorithm is done through feature flags. [#2501]
[#2501]: https://github.com/actix/actix-web/pull/2501
## 4.0.0-beta.18 - 2021-12-29 ## 4.0.0-beta.18 - 2021-12-29

View file

@ -420,7 +420,7 @@ impl NamedFile {
} }
if let Some(current_encoding) = self.encoding { if let Some(current_encoding) = self.encoding {
res.encoding(current_encoding); res.encode_with(current_encoding);
} }
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file); let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
@ -494,7 +494,7 @@ impl NamedFile {
// default compressing // default compressing
if let Some(current_encoding) = self.encoding { if let Some(current_encoding) = self.encoding {
res.encoding(current_encoding); res.encode_with(current_encoding);
} }
if let Some(lm) = last_modified { if let Some(lm) = last_modified {
@ -517,7 +517,7 @@ impl NamedFile {
length = ranges[0].length; length = ranges[0].length;
offset = ranges[0].start; offset = ranges[0].start;
res.encoding(ContentEncoding::Identity); res.encode_with(ContentEncoding::Identity);
res.insert_header(( res.insert_header((
header::CONTENT_RANGE, header::CONTENT_RANGE,
format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()), format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),

View file

@ -1,8 +1,29 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
### Added
- `impl Eq` for `header::ContentEncoding`. [#2501]
- `impl Copy` for `QualityItem` where `T: Copy`. [#2501]
- `Quality::ZERO` equivalent to `q=0`. [#2501]
- `QualityItem::zero` that uses `Quality::ZERO`. [#2501]
- `ContentEncoding::to_header_value()`. [#2501]
### Changed
- `Quality::MIN` is now the smallest non-zero value. [#2501]
- `QualityItem::min` semantics changed with `QualityItem::MIN`. [#2501]
- Rename `ContentEncoding::{Br => Brotli}`. [#2501]
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.
### Fixed
- `ContentEncoding::Identity` can now be parsed from a string. [#2501]
- A `Vary` header is now correctly sent along with compressed content. [#2501]
### Removed
- `ContentEncoding::Auto` variant. [#2501]
- `ContentEncoding::is_compression()`. [#2501]
[#2501]: https://github.com/actix/actix-web/pull/2501
## 3.0.0-beta.17 - 2021-12-27 ## 3.0.0-beta.17 - 2021-12-27
### Changes ### Changes

View file

@ -47,9 +47,9 @@ where
pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> { pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
let decoder = match encoding { let decoder = match encoding {
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(BrotliDecoder::new( ContentEncoding::Brotli => Some(ContentDecoder::Brotli(Box::new(
Writer::new(), BrotliDecoder::new(Writer::new()),
)))), ))),
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
ZlibDecoder::new(Writer::new()), ZlibDecoder::new(Writer::new()),
@ -165,7 +165,7 @@ enum ContentDecoder {
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
Gzip(Box<GzDecoder<Writer>>), Gzip(Box<GzDecoder<Writer>>),
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
Br(Box<BrotliDecoder<Writer>>), Brotli(Box<BrotliDecoder<Writer>>),
// We need explicit 'static lifetime here because ZstdDecoder need lifetime // 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` // argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static`
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
@ -176,7 +176,7 @@ impl ContentDecoder {
fn feed_eof(&mut self) -> io::Result<Option<Bytes>> { fn feed_eof(&mut self) -> io::Result<Option<Bytes>> {
match self { match self {
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentDecoder::Br(ref mut decoder) => match decoder.flush() { ContentDecoder::Brotli(ref mut decoder) => match decoder.flush() {
Ok(()) => { Ok(()) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
@ -234,7 +234,7 @@ impl ContentDecoder {
fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> { fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> {
match self { match self {
#[cfg(feature = "compress-brotli")] #[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(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
let b = decoder.get_mut().take(); 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 { 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::SWITCHING_PROTOCOLS
|| head.status == StatusCode::NO_CONTENT || head.status == StatusCode::NO_CONTENT
|| encoding == ContentEncoding::Identity || encoding == ContentEncoding::Identity);
|| encoding == ContentEncoding::Auto);
// no need to compress an empty body // no need to compress an empty body
if matches!(body.size(), BodySize::None) { if matches!(body.size(), BodySize::None) {
@ -72,8 +71,8 @@ impl<B: MessageBody> Encoder<B> {
Err(body) => EncoderBody::Stream { body }, Err(body) => EncoderBody::Stream { body },
}; };
if can_encode { if should_encode {
// Modify response body only if encoder is set // wrap body only if encoder is feature-enabled
if let Some(enc) = ContentEncoder::encoder(encoding) { if let Some(enc) = ContentEncoder::encoder(encoding) {
update_head(encoding, head); update_head(encoding, head);
@ -252,10 +251,10 @@ where
} }
fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
head.headers_mut().insert( head.headers_mut()
header::CONTENT_ENCODING, .insert(header::CONTENT_ENCODING, encoding.to_header_value());
HeaderValue::from_static(encoding.as_str()), head.headers_mut()
); .insert(header::VARY, HeaderValue::from_static("accept-encoding"));
head.no_chunking(false); head.no_chunking(false);
} }
@ -268,7 +267,7 @@ enum ContentEncoder {
Gzip(GzEncoder<Writer>), Gzip(GzEncoder<Writer>),
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
Br(BrotliEncoder<Writer>), Brotli(BrotliEncoder<Writer>),
// Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we // 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`. // use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
@ -292,8 +291,8 @@ impl ContentEncoder {
))), ))),
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentEncoding::Br => { ContentEncoding::Brotli => {
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) Some(ContentEncoder::Brotli(BrotliEncoder::new(Writer::new(), 3)))
} }
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
@ -302,7 +301,7 @@ impl ContentEncoder {
Some(ContentEncoder::Zstd(encoder)) Some(ContentEncoder::Zstd(encoder))
} }
_ => None, ContentEncoding::Identity => None,
} }
} }
@ -310,7 +309,7 @@ impl ContentEncoder {
pub(crate) fn take(&mut self) -> Bytes { pub(crate) fn take(&mut self) -> Bytes {
match *self { match *self {
#[cfg(feature = "compress-brotli")] #[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")] #[cfg(feature = "compress-gzip")]
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
@ -326,7 +325,7 @@ impl ContentEncoder {
fn finish(self) -> Result<Bytes, io::Error> { fn finish(self) -> Result<Bytes, io::Error> {
match self { match self {
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentEncoder::Br(encoder) => match encoder.finish() { ContentEncoder::Brotli(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
@ -354,7 +353,7 @@ impl ContentEncoder {
fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
match *self { match *self {
#[cfg(feature = "compress-brotli")] #[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(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
trace!("Error decoding br encoding: {}", 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. /// A set of errors that can occur running blocking tasks in thread pool.
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[display(fmt = "Blocking thread pool is gone")] #[display(fmt = "Blocking thread pool is gone")]
// TODO: non-exhaustive
pub struct BlockingError; pub struct BlockingError;
/// A set of errors that can occur during payload parsing. /// 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. /// Iterator over removed, owned values with the same associated name.
/// ///
/// Returned from methods that remove or replace items. See [`HeaderMap::insert`] /// 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>; 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 /// This encode set is used for HTTP header values and is defined at
/// <https://datatracker.ietf.org/doc/html/rfc5987#section-3.2>. /// <https://datatracker.ietf.org/doc/html/rfc5987#section-3.2>.
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS

View file

@ -20,14 +20,16 @@ pub struct ContentEncodingParseError;
/// See [IANA HTTP Content Coding Registry]. /// See [IANA HTTP Content Coding Registry].
/// ///
/// [IANA HTTP Content Coding Registry]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml /// [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] #[non_exhaustive]
pub enum ContentEncoding { pub enum ContentEncoding {
/// Automatically select encoding based on encoding negotiation. /// Indicates the no-op identity encoding.
Auto, ///
/// I.e., no compression or modification.
Identity,
/// A format using the Brotli algorithm. /// A format using the Brotli algorithm.
Br, Brotli,
/// A format using the zlib structure with deflate algorithm. /// A format using the zlib structure with deflate algorithm.
Deflate, Deflate,
@ -37,27 +39,30 @@ pub enum ContentEncoding {
/// Zstd algorithm. /// Zstd algorithm.
Zstd, Zstd,
/// Indicates the identity function (i.e. no compression, nor modification).
Identity,
} }
impl ContentEncoding { 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. /// Convert content encoding to string.
#[inline] #[inline]
pub const fn as_str(self) -> &'static str { pub const fn as_str(self) -> &'static str {
match self { match self {
ContentEncoding::Br => "br", ContentEncoding::Brotli => "br",
ContentEncoding::Gzip => "gzip", ContentEncoding::Gzip => "gzip",
ContentEncoding::Deflate => "deflate", ContentEncoding::Deflate => "deflate",
ContentEncoding::Zstd => "zstd", 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 { impl FromStr for ContentEncoding {
type Err = ContentEncodingParseError; type Err = ContentEncodingParseError;
fn from_str(val: &str) -> Result<Self, Self::Err> { fn from_str(enc: &str) -> Result<Self, Self::Err> {
let val = val.trim(); let enc = enc.trim();
if val.eq_ignore_ascii_case("br") { if enc.eq_ignore_ascii_case("br") {
Ok(ContentEncoding::Br) Ok(ContentEncoding::Brotli)
} else if val.eq_ignore_ascii_case("gzip") { } else if enc.eq_ignore_ascii_case("gzip") {
Ok(ContentEncoding::Gzip) Ok(ContentEncoding::Gzip)
} else if val.eq_ignore_ascii_case("deflate") { } else if enc.eq_ignore_ascii_case("deflate") {
Ok(ContentEncoding::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) Ok(ContentEncoding::Zstd)
} else { } else {
Err(ContentEncodingParseError) 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(0.42).to_string(), "0.42");
/// assert_eq!(q(1.0).to_string(), "1"); /// 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 /// [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`. /// The maximum quality value, equivalent to `q=1.0`.
pub const MAX: Quality = Quality(MAX_QUALITY_INT); pub const MAX: Quality = Quality(MAX_QUALITY_INT);
/// The minimum quality value, equivalent to `q=0.0`. /// The minimum, non-zero quality value, equivalent to `q=0.001`.
pub const MIN: Quality = Quality(0); 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`. /// 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. // 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. // Just in case, this debug_assert should catch if we were forgetful.
debug_assert!( 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" "q value must be between 0.0 and 1.0"
); );
@ -154,10 +158,13 @@ impl TryFrom<f32> for Quality {
/// let q1 = q(1.0); /// let q1 = q(1.0);
/// assert_eq!(q1, Quality::MAX); /// assert_eq!(q1, Quality::MAX);
/// ///
/// let q2 = q(0.0); /// let q2 = q(0.001);
/// assert_eq!(q2, Quality::MIN); /// 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. /// An out-of-range `f32` quality will panic.
@ -185,6 +192,10 @@ mod tests {
#[test] #[test]
fn display_output() { 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(0.0).to_string(), "0");
assert_eq!(q(1.0).to_string(), "1"); assert_eq!(q(1.0).to_string(), "1");
assert_eq!(q(0.001).to_string(), "0.001"); 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(); /// let q_item_fallback: QualityItem<String> = "abc;q=0.1".parse().unwrap();
/// assert!(q_item > q_item_fallback); /// assert!(q_item > q_item_fallback);
/// ``` /// ```
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QualityItem<T> { pub struct QualityItem<T> {
/// The wrapped contents of the field. /// The wrapped contents of the field.
pub item: T, pub item: T,
@ -53,10 +53,15 @@ impl<T> QualityItem<T> {
Self::new(item, Quality::MAX) 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 { pub fn min(item: T) -> Self {
Self::new(item, Quality::MIN) 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> { 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 // q-factor value is implied for max value
Quality::MAX => Ok(()), 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), q => write!(f, "; q={}", q),
} }
} }

View file

@ -473,7 +473,7 @@ async fn test_no_decompress() {
.wrap(actix_web::middleware::Compress::default()) .wrap(actix_web::middleware::Compress::default())
.service(web::resource("/").route(web::to(|| { .service(web::resource("/").route(web::to(|| {
let mut res = HttpResponse::Ok().body(STR); let mut res = HttpResponse::Ok().body(STR);
res.encoding(header::ContentEncoding::Gzip); res.encode_with(header::ContentEncoding::Gzip);
res res
}))) })))
}); });
@ -644,7 +644,9 @@ async fn test_client_brotli_encoding_large_random() {
async fn test_client_deflate_encoding() { async fn test_client_deflate_encoding() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new().default_service(web::to(|body: Bytes| { App::new().default_service(web::to(|body: Bytes| {
HttpResponse::Ok().encoding(ContentEncoding::Br).body(body) HttpResponse::Ok()
.encode_with(ContentEncoding::Brotli)
.body(body)
})) }))
}); });
@ -667,7 +669,9 @@ async fn test_client_deflate_encoding_large_random() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new().default_service(web::to(|body: Bytes| { App::new().default_service(web::to(|body: Bytes| {
HttpResponse::Ok().encoding(ContentEncoding::Br).body(body) HttpResponse::Ok()
.encode_with(ContentEncoding::Brotli)
.body(body)
})) }))
}); });
@ -685,7 +689,7 @@ async fn test_client_streaming_explicit() {
let srv = actix_test::start(|| { let srv = actix_test::start(|| {
App::new().default_service(web::to(|body: web::Payload| { App::new().default_service(web::to(|body: web::Payload| {
HttpResponse::Ok() HttpResponse::Ok()
.encoding(ContentEncoding::Identity) .encode_with(ContentEncoding::Identity)
.streaming(body) .streaming(body)
})) }))
}); });
@ -710,7 +714,7 @@ async fn test_body_streaming_implicit() {
}); });
HttpResponse::Ok() HttpResponse::Ok()
.encoding(ContentEncoding::Gzip) .encode_with(ContentEncoding::Gzip)
.streaming(Box::pin(body)) .streaming(Box::pin(body))
})) }))
}); });

View file

@ -12,16 +12,16 @@ save_exit_code() {
[ "$CMD_EXIT" = "0" ] || EXIT=$CMD_EXIT [ "$CMD_EXIT" = "0" ] || EXIT=$CMD_EXIT
} }
save_exit_code cargo test --lib --tests -p=actix-router --all-features save_exit_code cargo test --lib --tests -p=actix-router --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-http --all-features save_exit_code cargo test --lib --tests -p=actix-http --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --nocapture --skip=test_reading_deflate_encoding_large_random_rustls
save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=awc --all-features save_exit_code cargo test --lib --tests -p=awc --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-http-test --all-features save_exit_code cargo test --lib --tests -p=actix-http-test --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-test --all-features save_exit_code cargo test --lib --tests -p=actix-test --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-files save_exit_code cargo test --lib --tests -p=actix-files -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-multipart --all-features save_exit_code cargo test --lib --tests -p=actix-multipart --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features -- --nocapture
save_exit_code cargo test --workspace --doc save_exit_code cargo test --workspace --doc

View file

@ -20,11 +20,9 @@ pub use crate::info::{ConnectionInfo, PeerAddr};
pub use crate::rmap::ResourceMap; pub use crate::rmap::ResourceMap;
pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService}; pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService};
pub use crate::types::form::UrlEncoded; pub use crate::types::{JsonBody, Readlines, UrlEncoded};
pub use crate::types::json::JsonBody;
pub use crate::types::readlines::Readlines;
use crate::http::header::ContentEncoding; use crate::{http::header::ContentEncoding, HttpMessage as _};
use actix_router::Patterns; use actix_router::Patterns;
@ -47,59 +45,109 @@ pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns {
patterns patterns
} }
/// Helper trait that allows to set specific encoding for response. /// Helper trait for managing response encoding.
///
/// Use `pre_encoded_with` to flag response as already encoded. For example, when serving a Gzip
/// compressed file from disk.
pub trait BodyEncoding { pub trait BodyEncoding {
/// Get content encoding /// Get content encoding
fn get_encoding(&self) -> Option<ContentEncoding>; fn preferred_encoding(&self) -> Option<ContentEncoding>;
/// Set content encoding /// Set content encoding to use.
/// ///
/// Must be used with [`crate::middleware::Compress`] to take effect. /// Must be used with [`Compress`] to take effect.
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self; ///
/// [`Compress`]: crate::middleware::Compress
fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self;
// /// Flags that a file already is encoded so that [`Compress`] does not modify it.
// ///
// /// Effectively a shortcut for `compress_with("identity")`
// /// plus `insert_header(ContentEncoding, encoding)`.
// ///
// /// [`Compress`]: crate::middleware::Compress
// fn pre_encoded_with(&mut self, encoding: ContentEncoding) -> &mut Self;
} }
impl BodyEncoding for actix_http::ResponseBuilder { struct CompressWith(ContentEncoding);
fn get_encoding(&self) -> Option<ContentEncoding> {
self.extensions().get::<Enc>().map(|enc| enc.0)
}
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { // TODO: add or delete this
self.extensions_mut().insert(Enc(encoding)); // struct PreCompressed(ContentEncoding);
self
}
}
struct Enc(ContentEncoding);
impl<B> BodyEncoding for actix_http::Response<B> {
fn get_encoding(&self) -> Option<ContentEncoding> {
self.extensions().get::<Enc>().map(|enc| enc.0)
}
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self {
self.extensions_mut().insert(Enc(encoding));
self
}
}
impl BodyEncoding for crate::HttpResponseBuilder { impl BodyEncoding for crate::HttpResponseBuilder {
fn get_encoding(&self) -> Option<ContentEncoding> { fn preferred_encoding(&self) -> Option<ContentEncoding> {
self.extensions().get::<Enc>().map(|enc| enc.0) self.extensions().get::<CompressWith>().map(|enc| enc.0)
} }
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self {
self.extensions_mut().insert(Enc(encoding)); self.extensions_mut().insert(CompressWith(encoding));
self self
} }
// fn pre_encoded_with(&mut self, encoding: ContentEncoding) -> &mut Self {
// self.extensions_mut().insert(PreCompressed(encoding));
// self
// }
} }
impl<B> BodyEncoding for crate::HttpResponse<B> { impl<B> BodyEncoding for crate::HttpResponse<B> {
fn get_encoding(&self) -> Option<ContentEncoding> { fn preferred_encoding(&self) -> Option<ContentEncoding> {
self.extensions().get::<Enc>().map(|enc| enc.0) self.extensions().get::<CompressWith>().map(|enc| enc.0)
} }
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self {
self.extensions_mut().insert(Enc(encoding)); self.extensions_mut().insert(CompressWith(encoding));
self
}
// fn pre_encoded_with(&mut self, encoding: ContentEncoding) -> &mut Self {
// self.extensions_mut().insert(PreCompressed(encoding));
// self
// }
}
impl<B> BodyEncoding for ServiceResponse<B> {
fn preferred_encoding(&self) -> Option<ContentEncoding> {
self.request()
.extensions()
.get::<CompressWith>()
.map(|enc| enc.0)
}
fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self {
self.request()
.extensions_mut()
.insert(CompressWith(encoding));
self
}
// fn pre_encoded_with(&mut self, encoding: ContentEncoding) -> &mut Self {
// self.request()
// .extensions_mut()
// .insert(PreCompressed(encoding));
// self
// }
}
// TODO: remove these impls ?
impl BodyEncoding for actix_http::ResponseBuilder {
fn preferred_encoding(&self) -> Option<ContentEncoding> {
self.extensions().get::<CompressWith>().map(|enc| enc.0)
}
fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self {
self.extensions_mut().insert(CompressWith(encoding));
self
}
}
impl<B> BodyEncoding for actix_http::Response<B> {
fn preferred_encoding(&self) -> Option<ContentEncoding> {
self.extensions().get::<CompressWith>().map(|enc| enc.0)
}
fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self {
self.extensions_mut().insert(CompressWith(encoding));
self self
} }
} }

View file

@ -147,6 +147,39 @@ impl Accept {
Accept(vec![QualityItem::max(mime::TEXT_HTML)]) Accept(vec![QualityItem::max(mime::TEXT_HTML)])
} }
// TODO: method for getting best content encoding based on q-factors, available from server side
// and if none are acceptable return None
/// Extracts the most preferable mime type, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first mime type is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained
/// list is empty.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Mime {
use actix_http::header::Quality;
let mut max_item = None;
let mut max_pref = Quality::ZERO;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
max_item.unwrap_or(mime::STAR_STAR)
}
/// Returns a sorted list of mime types from highest to lowest preference, accounting for /// Returns a sorted list of mime types from highest to lowest preference, accounting for
/// [q-factor weighting] and specificity. /// [q-factor weighting] and specificity.
/// ///
@ -196,36 +229,6 @@ impl Accept {
types.into_iter().map(|qitem| qitem.item).collect() types.into_iter().map(|qitem| qitem.item).collect()
} }
/// Extracts the most preferable mime type, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first mime type is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained
/// list is empty.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Mime {
use actix_http::header::Quality;
let mut max_item = None;
let mut max_pref = Quality::MIN;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
max_item.unwrap_or(mime::STAR_STAR)
}
} }
#[cfg(test)] #[cfg(test)]
@ -239,7 +242,7 @@ mod tests {
assert!(test.ranked().is_empty()); assert!(test.ranked().is_empty());
let test = Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]); let test = Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]);
assert_eq!(test.ranked(), vec!(mime::APPLICATION_JSON)); assert_eq!(test.ranked(), vec![mime::APPLICATION_JSON]);
let test = Accept(vec![ let test = Accept(vec![
QualityItem::max(mime::TEXT_HTML), QualityItem::max(mime::TEXT_HTML),

View file

@ -1,8 +1,7 @@
use super::{Charset, QualityItem, ACCEPT_CHARSET}; use super::{common_header, Charset, QualityItem, ACCEPT_CHARSET};
crate::http::header::common_header! { common_header! {
/// `Accept-Charset` header, defined in /// `Accept-Charset` header, defined in [RFC 7231 §5.3.3].
/// [RFC 7231 §5.3.3](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3)
/// ///
/// The `Accept-Charset` header field can be sent by a user agent to /// The `Accept-Charset` header field can be sent by a user agent to
/// indicate what charsets are acceptable in textual response content. /// indicate what charsets are acceptable in textual response content.
@ -52,10 +51,12 @@ crate::http::header::common_header! {
/// AcceptCharset(vec![QualityItem::max(Charset::Ext("utf-8".to_owned()))]) /// AcceptCharset(vec![QualityItem::max(Charset::Ext("utf-8".to_owned()))])
/// ); /// );
/// ``` /// ```
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)+ ///
/// [RFC 7231 §5.3.3]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)*
test_parse_and_format { test_parse_and_format {
// Test case from RFC // Test case from RFC
crate::http::header::common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]);
} }
} }

View file

@ -1,17 +1,15 @@
use actix_http::header::QualityItem; use std::collections::HashSet;
use super::{common_header, Encoding}; use super::{common_header, ContentEncoding, Encoding, Preference, Quality, QualityItem};
use crate::http::header; use crate::http::header;
common_header! { common_header! {
/// `Accept-Encoding` header, defined /// `Accept-Encoding` header, defined
/// in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4) /// in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4)
/// ///
/// The `Accept-Encoding` header field can be used by user agents to /// The `Accept-Encoding` header field can be used by user agents to indicate what response
/// indicate what response content-codings are /// content-codings are acceptable in the response. An `identity` token is used as a synonym
/// acceptable in the response. An `identity` token is used as a synonym /// for "no encoding" in order to communicate when no encoding is preferred.
/// for "no encoding" in order to communicate when no encoding is
/// preferred.
/// ///
/// # ABNF /// # ABNF
/// ```plain /// ```plain
@ -29,11 +27,11 @@ common_header! {
/// # Examples /// # Examples
/// ``` /// ```
/// use actix_web::HttpResponse; /// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem}; /// use actix_web::http::header::{AcceptEncoding, Encoding, Preference, QualityItem};
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = HttpResponse::Ok();
/// builder.insert_header( /// builder.insert_header(
/// AcceptEncoding(vec![QualityItem::max(Encoding::Chunked)]) /// AcceptEncoding(vec![QualityItem::max(Preference::Specific(Encoding::gzip()))])
/// ); /// );
/// ``` /// ```
/// ///
@ -44,40 +42,388 @@ common_header! {
/// let mut builder = HttpResponse::Ok(); /// let mut builder = HttpResponse::Ok();
/// builder.insert_header( /// builder.insert_header(
/// AcceptEncoding(vec![ /// AcceptEncoding(vec![
/// QualityItem::max(Encoding::Chunked), /// "gzip".parse().unwrap(),
/// QualityItem::max(Encoding::Gzip), /// "br".parse().unwrap(),
/// QualityItem::max(Encoding::Deflate),
/// ]) /// ])
/// ); /// );
/// ``` /// ```
/// (AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem<Preference<Encoding>>)*
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem, q};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptEncoding(vec![
/// QualityItem::max(Encoding::Chunked),
/// QualityItem::new(Encoding::Gzip, q(0.60)),
/// QualityItem::min(Encoding::EncodingExt("*".to_owned())),
/// ])
/// );
/// ```
(AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem<Encoding>)*
test_parse_and_format { test_parse_and_format {
// From the RFC common_header_test!(no_headers, vec![b""; 0], Some(AcceptEncoding(vec![])));
common_header_test!(test1, vec![b"compress, gzip"]); common_header_test!(empty_header, vec![b""; 1], Some(AcceptEncoding(vec![])));
common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![])));
common_header_test!(test3, vec![b"*"]); common_header_test!(
order_of_appearance,
vec![b"br, gzip"],
Some(AcceptEncoding(vec![
QualityItem::max(Preference::Specific(Encoding::brotli())),
QualityItem::max(Preference::Specific(Encoding::gzip())),
]))
);
common_header_test!(any, vec![b"*"], Some(AcceptEncoding(vec![
QualityItem::max(Preference::Any),
])));
// Note: Removed quality 1 from gzip // Note: Removed quality 1 from gzip
common_header_test!(test4, vec![b"compress;q=0.5, gzip"]); common_header_test!(implicit_quality, vec![b"gzip, identity; q=0.5, *;q=0"]);
// Note: Removed quality 1 from gzip // Note: Removed quality 1 from gzip
common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); common_header_test!(implicit_quality_out_of_order, vec![b"compress;q=0.5, gzip"]);
common_header_test!(
only_gzip_no_identity,
vec![b"gzip, *; q=0"],
Some(AcceptEncoding(vec![
QualityItem::max(Preference::Specific(Encoding::gzip())),
QualityItem::zero(Preference::Any),
]))
);
} }
} }
// TODO: shortcut for EncodingExt(*) = Any impl AcceptEncoding {
/// Selects the most acceptable encoding according to client preference and supported types.
///
/// The "identity" encoding is not assumed and should be included in the `supported` iterator
/// if a non-encoded representation can be selected.
///
/// If `None` is returned, this indicates that none of the supported encodings are acceptable to
/// the client. The caller should generate a 406 Not Acceptable response (unencoded) that
/// includes the server's supported encodings in the body plus a [`Vary`] header.
///
/// [`Vary`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
pub fn negotiate<'a>(
&self,
supported: impl Iterator<Item = &'a Encoding>,
) -> Option<Encoding> {
// 1. If no Accept-Encoding field is in the request, any content-coding is considered
// acceptable by the user agent.
let supported_set = supported.collect::<HashSet<_>>();
if supported_set.is_empty() {
return None;
}
if self.0.is_empty() {
// though it is not recommended to encode in this case, return identity encoding
return Some(Encoding::identity());
}
// 2. If the representation has no content-coding, then it is acceptable by default unless
// specifically excluded by the Accept-Encoding field stating either "identity;q=0" or
// "*;q=0" without a more specific entry for "identity".
let acceptable_items = self.ranked_items().collect::<Vec<_>>();
let identity_acceptable = is_identity_acceptable(&acceptable_items);
let identity_supported = supported_set.contains(&Encoding::identity());
if identity_acceptable && identity_supported && supported_set.len() == 1 {
return Some(Encoding::identity());
}
// 3. If the representation's content-coding is one of the content-codings listed in the
// Accept-Encoding field, then it is acceptable unless it is accompanied by a qvalue of 0.
// 4. If multiple content-codings are acceptable, then the acceptable content-coding with
// the highest non-zero qvalue is preferred.
let matched = acceptable_items
.into_iter()
.filter(|q| q.quality > Quality::ZERO)
// search relies on item list being in descending order of quality
.find(|q| {
let enc = &q.item;
matches!(enc, Preference::Specific(enc) if supported_set.contains(enc))
})
.map(|q| q.item);
match matched {
Some(Preference::Specific(enc)) => Some(enc),
_ if identity_acceptable => Some(Encoding::identity()),
_ => None,
}
}
/// Extracts the most preferable encoding, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first encoding is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, returns [`Preference::Any`] if acceptable list is empty. Though, if this is
/// returned, it is recommended to use an un-encoded representation.
///
/// If `None` is returned, it means that the client has signalled that no representations
/// are acceptable. This should never occur for a well behaved user-agent.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Option<Preference<Encoding>> {
// empty header indicates no preference
if self.0.is_empty() {
return Some(Preference::Any);
}
let mut max_item = None;
let mut max_pref = Quality::ZERO;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
// Return max_item if any items were above 0 quality...
max_item.or_else(|| {
// ...or else check for "*" or "identity". We can elide quality checks since
// entering this block means all items had "q=0".
match self.0.iter().find(|pref| {
matches!(
pref.item,
Preference::Any
| Preference::Specific(Encoding::Known(ContentEncoding::Identity))
)
}) {
// "identity" or "*" found so no representation is acceptable
Some(_) => None,
// implicit "identity" is acceptable
None => Some(Preference::Specific(Encoding::identity())),
}
})
}
/// Returns a sorted list of encodings from highest to lowest precedence, accounting
/// for [q-factor weighting].
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn ranked(&self) -> Vec<Preference<Encoding>> {
self.ranked_items().map(|q| q.item).collect()
}
fn ranked_items(&self) -> impl Iterator<Item = QualityItem<Preference<Encoding>>> {
if self.0.is_empty() {
return vec![].into_iter();
}
let mut types = self.0.clone();
// use stable sort so items with equal q-factor retain listed order
types.sort_by(|a, b| {
// sort by q-factor descending
b.quality.cmp(&a.quality)
});
types.into_iter()
}
}
/// Returns true if "identity" is an acceptable encoding.
///
/// Internal algorithm relies on item list being in descending order of quality.
fn is_identity_acceptable(items: &'_ [QualityItem<Preference<Encoding>>]) -> bool {
if items.is_empty() {
return true;
}
// Loop algorithm depends on items being sorted in descending order of quality. As such, it
// is sufficient to return (q > 0) when reaching either an "identity" or "*" item.
for q in items {
match (q.quality, &q.item) {
// occurrence of "identity;q=n"; return true if quality is non-zero
(q, Preference::Specific(Encoding::Known(ContentEncoding::Identity))) => {
return q > Quality::ZERO
}
// occurrence of "*;q=n"; return true if quality is non-zero
(q, Preference::Any) => return q > Quality::ZERO,
_ => {}
}
}
// implicit acceptable identity
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::header::*;
macro_rules! accept_encoding {
() => { AcceptEncoding(vec![]) };
($($q:expr),+ $(,)?) => { AcceptEncoding(vec![$($q.parse().unwrap()),+]) };
}
/// Parses an encoding string.
fn enc(enc: &str) -> Preference<Encoding> {
enc.parse().unwrap()
}
#[test]
fn detect_identity_acceptable() {
macro_rules! accept_encoding_ranked {
() => { accept_encoding!().ranked_items().collect::<Vec<_>>() };
($($q:expr),+ $(,)?) => { accept_encoding!($($q),+).ranked_items().collect::<Vec<_>>() };
}
let test = accept_encoding_ranked!();
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "br");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0.1");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0.1");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0.1", "*;q=0");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0.1");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0");
assert!(!is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0");
assert!(!is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0", "*;q=0");
assert!(!is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0");
assert!(!is_identity_acceptable(&test));
}
#[test]
fn encoding_negotiation() {
// no preference
let test = accept_encoding!();
assert_eq!(test.negotiate([].iter()), None);
let test = accept_encoding!();
assert_eq!(
test.negotiate([Encoding::identity()].iter()),
Some(Encoding::identity()),
);
let test = accept_encoding!("identity;q=0");
assert_eq!(test.negotiate([Encoding::identity()].iter()), None);
let test = accept_encoding!("*;q=0");
assert_eq!(test.negotiate([Encoding::identity()].iter()), None);
let test = accept_encoding!();
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::identity()),
);
let test = accept_encoding!("gzip");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
Some(Encoding::identity()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
let test = accept_encoding!("gzip", "identity;q=0");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
None
);
let test = accept_encoding!("gzip", "*;q=0");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
None
);
let test = accept_encoding!("gzip", "deflate", "br");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
Some(Encoding::brotli())
);
assert_eq!(
test.negotiate([Encoding::deflate(), Encoding::identity()].iter()),
Some(Encoding::deflate())
);
assert_eq!(
test.negotiate(
[Encoding::gzip(), Encoding::deflate(), Encoding::identity()].iter()
),
Some(Encoding::gzip())
);
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::brotli(), Encoding::identity()].iter()),
Some(Encoding::gzip())
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip())
);
}
#[test]
fn ranking_precedence() {
let test = accept_encoding!();
assert!(test.ranked().is_empty());
let test = accept_encoding!("gzip");
assert_eq!(test.ranked(), vec![enc("gzip")]);
let test = accept_encoding!("gzip;q=0.900", "*;q=0.700", "br;q=1.0");
assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
let test = accept_encoding!("br", "gzip", "*");
assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
}
#[test]
fn preference_selection() {
assert_eq!(accept_encoding!().preference(), Some(Preference::Any));
assert_eq!(accept_encoding!("identity;q=0").preference(), None);
assert_eq!(accept_encoding!("*;q=0").preference(), None);
assert_eq!(accept_encoding!("compress;q=0", "*;q=0").preference(), None);
assert_eq!(accept_encoding!("identity;q=0", "*;q=0").preference(), None);
let test = accept_encoding!("*;q=0.5");
assert_eq!(test.preference().unwrap(), enc("*"));
let test = accept_encoding!("br;q=0");
assert_eq!(test.preference().unwrap(), enc("identity"));
let test = accept_encoding!("br;q=0.900", "gzip;q=1.0", "*;q=0.500");
assert_eq!(test.preference().unwrap(), enc("gzip"));
let test = accept_encoding!("br", "gzip", "*");
assert_eq!(test.preference().unwrap(), enc("br"));
}
}

View file

@ -37,7 +37,7 @@ common_header! {
/// let mut builder = HttpResponse::Ok(); /// let mut builder = HttpResponse::Ok();
/// builder.insert_header( /// builder.insert_header(
/// AcceptLanguage(vec![ /// AcceptLanguage(vec![
/// QualityItem::max("en-US".parse().unwrap()) /// "en-US".parse().unwrap(),
/// ]) /// ])
/// ); /// );
/// ``` /// ```
@ -49,9 +49,9 @@ common_header! {
/// let mut builder = HttpResponse::Ok(); /// let mut builder = HttpResponse::Ok();
/// builder.insert_header( /// builder.insert_header(
/// AcceptLanguage(vec![ /// AcceptLanguage(vec![
/// QualityItem::max("da".parse().unwrap()), /// "da".parse().unwrap(),
/// QualityItem::new("en-GB".parse().unwrap(), q(0.8)), /// "en-GB;q=0.8".parse().unwrap(),
/// QualityItem::new("en".parse().unwrap(), q(0.7)), /// "en;q=0.7".parse().unwrap(),
/// ]) /// ])
/// ); /// );
/// ``` /// ```
@ -93,6 +93,33 @@ common_header! {
} }
impl AcceptLanguage { impl AcceptLanguage {
/// Extracts the most preferable language, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first language is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, returns [`Preference::Any`] if contained list is empty.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Preference<LanguageTag> {
let mut max_item = None;
let mut max_pref = Quality::ZERO;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
max_item.unwrap_or(Preference::Any)
}
/// Returns a sorted list of languages from highest to lowest precedence, accounting /// Returns a sorted list of languages from highest to lowest precedence, accounting
/// for [q-factor weighting]. /// for [q-factor weighting].
/// ///
@ -112,33 +139,6 @@ impl AcceptLanguage {
types.into_iter().map(|qitem| qitem.item).collect() types.into_iter().map(|qitem| qitem.item).collect()
} }
/// Extracts the most preferable language, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first language is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, returns [`Preference::Any`] if contained list is empty.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Preference<LanguageTag> {
let mut max_item = None;
let mut max_pref = Quality::MIN;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
max_item.unwrap_or(Preference::Any)
}
} }
#[cfg(test)] #[cfg(test)]
@ -152,7 +152,7 @@ mod tests {
assert!(test.ranked().is_empty()); assert!(test.ranked().is_empty());
let test = AcceptLanguage(vec![QualityItem::max("fr-CH".parse().unwrap())]); let test = AcceptLanguage(vec![QualityItem::max("fr-CH".parse().unwrap())]);
assert_eq!(test.ranked(), vec!("fr-CH".parse().unwrap())); assert_eq!(test.ranked(), vec!["fr-CH".parse().unwrap()]);
let test = AcceptLanguage(vec![ let test = AcceptLanguage(vec![
QualityItem::new("fr".parse().unwrap(), q(0.900)), QualityItem::new("fr".parse().unwrap(), q(0.900)),

View file

@ -301,7 +301,6 @@ impl DispositionParam {
/// change to match local file system conventions if applicable, and do not use directory path /// change to match local file system conventions if applicable, and do not use directory path
/// information that may be present. /// information that may be present.
/// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3). /// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3).
// TODO: think about using private fields and smallvec
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct ContentDisposition { pub struct ContentDisposition {
/// The disposition type /// The disposition type

View file

@ -1,69 +1,55 @@
use std::{fmt, str}; use std::{fmt, str};
pub use self::Encoding::{ use actix_http::ContentEncoding;
Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, Zstd,
};
/// A value to represent an encoding used in `Transfer-Encoding` or `Accept-Encoding` header. /// A value to represent an encoding used in the `Accept-Encoding` and `Content-Encoding` header.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Encoding { pub enum Encoding {
/// The `chunked` encoding. /// A supported content encoding. See [`ContentEncoding`] for variants.
Chunked, Known(ContentEncoding),
/// The `br` encoding. /// Some other encoding that is less common, can be any string.
Brotli, Unknown(String),
}
/// The `gzip` encoding. impl Encoding {
Gzip, pub const fn identity() -> Self {
Self::Known(ContentEncoding::Identity)
}
/// The `deflate` encoding. pub const fn brotli() -> Self {
Deflate, Self::Known(ContentEncoding::Brotli)
}
/// The `compress` encoding. pub const fn deflate() -> Self {
Compress, Self::Known(ContentEncoding::Deflate)
}
/// The `identity` encoding. pub const fn gzip() -> Self {
Identity, Self::Known(ContentEncoding::Gzip)
}
/// The `trailers` encoding. pub const fn zstd() -> Self {
Trailers, Self::Known(ContentEncoding::Zstd)
}
/// The `zstd` encoding.
Zstd,
/// Some other encoding that is less common, can be any String.
EncodingExt(String),
} }
impl fmt::Display for Encoding { impl fmt::Display for Encoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match *self { f.write_str(match self {
Chunked => "chunked", Encoding::Known(enc) => enc.as_str(),
Brotli => "br", Encoding::Unknown(enc) => enc.as_str(),
Gzip => "gzip",
Deflate => "deflate",
Compress => "compress",
Identity => "identity",
Trailers => "trailers",
Zstd => "zstd",
EncodingExt(ref s) => s.as_ref(),
}) })
} }
} }
impl str::FromStr for Encoding { impl str::FromStr for Encoding {
type Err = crate::error::ParseError; type Err = crate::error::ParseError;
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
match s { fn from_str(enc: &str) -> Result<Self, crate::error::ParseError> {
"chunked" => Ok(Chunked), match enc.parse::<ContentEncoding>() {
"br" => Ok(Brotli), Ok(enc) => Ok(Self::Known(enc)),
"deflate" => Ok(Deflate), Err(_) => Ok(Self::Unknown(enc.to_owned())),
"gzip" => Ok(Gzip),
"compress" => Ok(Compress),
"identity" => Ok(Identity),
"trailers" => Ok(Trailers),
"zstd" => Ok(Zstd),
_ => Ok(EncodingExt(s.to_owned())),
} }
} }
} }

View file

@ -1,20 +1,13 @@
//! For middleware documentation, see [`Compress`]. //! For middleware documentation, see [`Compress`].
use std::{ use std::{
cmp,
convert::TryFrom as _,
future::Future, future::Future,
marker::PhantomData, marker::PhantomData,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
use actix_http::{ use actix_http::encoding::Encoder;
body::{EitherBody, MessageBody},
encoding::Encoder,
header::{ContentEncoding, ACCEPT_ENCODING},
StatusCode,
};
use actix_service::{Service, Transform}; use actix_service::{Service, Transform};
use actix_utils::future::{ok, Either, Ready}; use actix_utils::future::{ok, Either, Ready};
use futures_core::ready; use futures_core::ready;
@ -22,9 +15,14 @@ use once_cell::sync::Lazy;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use crate::{ use crate::{
dev::BodyEncoding, body::{EitherBody, MessageBody},
dev::BodyEncoding as _,
http::{
header::{self, AcceptEncoding, Encoding, HeaderValue},
StatusCode,
},
service::{ServiceRequest, ServiceResponse}, service::{ServiceRequest, ServiceResponse},
Error, HttpResponse, Error, HttpMessage, HttpResponse,
}; };
/// Middleware for compressing response payloads. /// Middleware for compressing response payloads.
@ -40,21 +38,9 @@ use crate::{
/// .wrap(middleware::Compress::default()) /// .wrap(middleware::Compress::default())
/// .default_service(web::to(|| HttpResponse::NotFound())); /// .default_service(web::to(|| HttpResponse::NotFound()));
/// ``` /// ```
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub struct Compress(ContentEncoding); #[non_exhaustive]
pub struct Compress;
impl Compress {
/// Create new `Compress` middleware with the specified encoding.
pub fn new(encoding: ContentEncoding) -> Self {
Compress(encoding)
}
}
impl Default for Compress {
fn default() -> Self {
Compress::new(ContentEncoding::Auto)
}
}
impl<S, B> Transform<S, ServiceRequest> for Compress impl<S, B> Transform<S, ServiceRequest> for Compress
where where
@ -68,44 +54,14 @@ where
type Future = Ready<Result<Self::Transform, Self::InitError>>; type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future { fn new_transform(&self, service: S) -> Self::Future {
ok(CompressMiddleware { ok(CompressMiddleware { service })
service,
encoding: self.0,
})
} }
} }
pub struct CompressMiddleware<S> { pub struct CompressMiddleware<S> {
service: S, service: S,
encoding: ContentEncoding,
} }
static SUPPORTED_ALGORITHM_NAMES: Lazy<String> = Lazy::new(|| {
#[allow(unused_mut)] // only unused when no compress features enabled
let mut encoding: Vec<&str> = vec![];
#[cfg(feature = "compress-brotli")]
{
encoding.push("br");
}
#[cfg(feature = "compress-gzip")]
{
encoding.push("gzip");
encoding.push("deflate");
}
#[cfg(feature = "compress-zstd")]
encoding.push("zstd");
assert!(
!encoding.is_empty(),
"encoding can not be empty unless __compress feature has been explicitly enabled by itself"
);
encoding.join(", ")
});
impl<S, B> Service<ServiceRequest> for CompressMiddleware<S> impl<S, B> Service<ServiceRequest> for CompressMiddleware<S>
where where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
@ -121,39 +77,43 @@ where
#[allow(clippy::borrow_interior_mutable_const)] #[allow(clippy::borrow_interior_mutable_const)]
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
// negotiate content-encoding // negotiate content-encoding
let encoding_result = req let accept_encoding = req.get_header::<AcceptEncoding>();
.headers()
.get(&ACCEPT_ENCODING)
.and_then(|val| val.to_str().ok())
.map(|enc| AcceptEncoding::try_parse(enc, self.encoding));
match encoding_result { let accept_encoding = match accept_encoding {
// Missing header => fallback to identity // missing header; fallback to identity
None => Either::left(CompressResponse { None => {
encoding: ContentEncoding::Identity, return Either::left(CompressResponse {
fut: self.service.call(req), encoding: Encoding::identity(),
_phantom: PhantomData, fut: self.service.call(req),
}), _phantom: PhantomData,
})
}
// Valid encoding // valid accept-encoding header
Some(Ok(encoding)) => Either::left(CompressResponse { Some(accept_encoding) => accept_encoding,
encoding, };
fut: self.service.call(req),
_phantom: PhantomData,
}),
// There is an HTTP header but we cannot match what client as asked for match accept_encoding.negotiate(SUPPORTED_ENCODINGS.iter()) {
Some(Err(_)) => { None => {
let res = HttpResponse::with_body( let mut res = HttpResponse::with_body(
StatusCode::NOT_ACCEPTABLE, StatusCode::NOT_ACCEPTABLE,
SUPPORTED_ALGORITHM_NAMES.clone(), SUPPORTED_ENCODINGS_STRING.as_str(),
); );
res.headers_mut()
.insert(header::VARY, HeaderValue::from_static("Accept-Encoding"));
Either::right(ok(req Either::right(ok(req
.into_response(res) .into_response(res)
.map_into_boxed_body() .map_into_boxed_body()
.map_into_right_body())) .map_into_right_body()))
} }
Some(encoding) => Either::left(CompressResponse {
fut: self.service.call(req),
encoding,
_phantom: PhantomData,
}),
} }
} }
} }
@ -165,7 +125,7 @@ pin_project! {
{ {
#[pin] #[pin]
fut: S::Future, fut: S::Future,
encoding: ContentEncoding, encoding: Encoding,
_phantom: PhantomData<B>, _phantom: PhantomData<B>,
} }
} }
@ -182,10 +142,15 @@ where
match ready!(this.fut.poll(cx)) { match ready!(this.fut.poll(cx)) {
Ok(resp) => { Ok(resp) => {
let enc = if let Some(enc) = resp.response().get_encoding() { let enc = if let Some(enc) = resp.response().preferred_encoding() {
enc enc
} else { } else {
*this.encoding match this.encoding {
Encoding::Known(enc) => *enc,
Encoding::Unknown(enc) => {
unimplemented!("encoding {} should not be here", enc);
}
}
}; };
Poll::Ready(Ok(resp.map_body(move |head, body| { Poll::Ready(Ok(resp.map_body(move |head, body| {
@ -198,178 +163,57 @@ where
} }
} }
struct AcceptEncoding { static SUPPORTED_ENCODINGS_STRING: Lazy<String> = Lazy::new(|| {
encoding: ContentEncoding, #[allow(unused_mut)] // only unused when no compress features enabled
// TODO: use Quality or QualityItem<ContentEncoding> let mut encoding: Vec<&str> = vec![];
quality: f64,
}
impl Eq for AcceptEncoding {} #[cfg(feature = "compress-brotli")]
{
impl Ord for AcceptEncoding { encoding.push("br");
#[allow(clippy::comparison_chain)]
fn cmp(&self, other: &AcceptEncoding) -> cmp::Ordering {
if self.quality > other.quality {
cmp::Ordering::Less
} else if self.quality < other.quality {
cmp::Ordering::Greater
} else {
cmp::Ordering::Equal
}
}
}
impl PartialOrd for AcceptEncoding {
fn partial_cmp(&self, other: &AcceptEncoding) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for AcceptEncoding {
fn eq(&self, other: &AcceptEncoding) -> bool {
self.encoding == other.encoding && self.quality == other.quality
}
}
/// Parse q-factor from quality strings.
///
/// If parse fail, then fallback to default value which is 1.
/// More details available here: <https://developer.mozilla.org/en-US/docs/Glossary/Quality_values>
fn parse_quality(parts: &[&str]) -> f64 {
for part in parts {
if part.trim().starts_with("q=") {
return part[2..].parse().unwrap_or(1.0);
}
} }
1.0 #[cfg(feature = "compress-gzip")]
} {
encoding.push("gzip");
#[derive(Debug, PartialEq, Eq)] encoding.push("deflate");
enum AcceptEncodingError {
/// This error occurs when client only support compressed response and server do not have any
/// algorithm that match client accepted algorithms.
CompressionAlgorithmMismatch,
}
impl AcceptEncoding {
fn new(tag: &str) -> Option<AcceptEncoding> {
let parts: Vec<&str> = tag.split(';').collect();
let encoding = match parts.len() {
0 => return None,
_ => match ContentEncoding::try_from(parts[0]) {
Err(_) => return None,
Ok(x) => x,
},
};
let quality = parse_quality(&parts[1..]);
if quality <= 0.0 || quality > 1.0 {
return None;
}
Some(AcceptEncoding { encoding, quality })
} }
/// Parse a raw Accept-Encoding header value into an ordered list then return the best match #[cfg(feature = "compress-zstd")]
/// based on middleware configuration. {
pub fn try_parse( encoding.push("zstd");
raw: &str,
encoding: ContentEncoding,
) -> Result<ContentEncoding, AcceptEncodingError> {
let mut encodings = raw
.replace(' ', "")
.split(',')
.filter_map(AcceptEncoding::new)
.collect::<Vec<_>>();
encodings.sort();
for enc in encodings {
if encoding == ContentEncoding::Auto || encoding == enc.encoding {
return Ok(enc.encoding);
}
}
// Special case if user cannot accept uncompressed data.
// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
// TODO: account for whitespace
if raw.contains("*;q=0") || raw.contains("identity;q=0") {
return Err(AcceptEncodingError::CompressionAlgorithmMismatch);
}
Ok(ContentEncoding::Identity)
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! assert_parse_eq {
($raw:expr, $result:expr) => {
assert_eq!(
AcceptEncoding::try_parse($raw, ContentEncoding::Auto),
Ok($result)
);
};
} }
macro_rules! assert_parse_fail { assert!(
($raw:expr) => { !encoding.is_empty(),
assert!(AcceptEncoding::try_parse($raw, ContentEncoding::Auto).is_err()); "encoding can not be empty unless __compress feature has been explicitly enabled by itself"
}; );
encoding.join(", ")
});
static SUPPORTED_ENCODINGS: Lazy<Vec<Encoding>> = Lazy::new(|| {
let mut encodings = vec![Encoding::identity()];
#[cfg(feature = "compress-brotli")]
{
encodings.push(Encoding::brotli());
} }
#[test] #[cfg(feature = "compress-gzip")]
fn test_parse_encoding() { {
// Test simple case encodings.push(Encoding::gzip());
assert_parse_eq!("br", ContentEncoding::Br); encodings.push(Encoding::deflate());
assert_parse_eq!("gzip", ContentEncoding::Gzip);
assert_parse_eq!("deflate", ContentEncoding::Deflate);
assert_parse_eq!("zstd", ContentEncoding::Zstd);
// Test space, trim, missing values
assert_parse_eq!("br,,,,", ContentEncoding::Br);
assert_parse_eq!("gzip , br, zstd", ContentEncoding::Gzip);
// Test float number parsing
assert_parse_eq!("br;q=1 ,", ContentEncoding::Br);
assert_parse_eq!("br;q=1.0 , br", ContentEncoding::Br);
// Test wildcard
assert_parse_eq!("*", ContentEncoding::Identity);
assert_parse_eq!("*;q=1.0", ContentEncoding::Identity);
} }
#[test] #[cfg(feature = "compress-zstd")]
fn test_parse_encoding_qfactor_ordering() { {
assert_parse_eq!("gzip, br, zstd", ContentEncoding::Gzip); encodings.push(Encoding::zstd());
assert_parse_eq!("zstd, br, gzip", ContentEncoding::Zstd);
assert_parse_eq!("gzip;q=0.4, br;q=0.6", ContentEncoding::Br);
assert_parse_eq!("gzip;q=0.8, br;q=0.4", ContentEncoding::Gzip);
} }
#[test] assert!(
fn test_parse_encoding_qfactor_invalid() { !encodings.is_empty(),
// Out of range "encodings can not be empty unless __compress feature has been explicitly enabled by itself"
assert_parse_eq!("gzip;q=-5.0", ContentEncoding::Identity); );
assert_parse_eq!("gzip;q=5.0", ContentEncoding::Identity);
// Disabled encodings
assert_parse_eq!("gzip;q=0", ContentEncoding::Identity); });
}
#[test]
fn test_parse_compression_required() {
// Check we fallback to identity if there is an unsupported compression algorithm
assert_parse_eq!("compress", ContentEncoding::Identity);
// User do not want any compression
assert_parse_fail!("compress, identity;q=0");
assert_parse_fail!("compress, identity;q=0.0");
assert_parse_fail!("compress, *;q=0");
assert_parse_fail!("compress, *;q=0.0");
}
}

View file

@ -1,19 +1,18 @@
//! Common extractors and responders. //! Common extractors and responders.
// TODO: review visibility
mod either; mod either;
pub(crate) mod form; mod form;
mod header; mod header;
pub(crate) mod json; mod json;
mod path; mod path;
pub(crate) mod payload; mod payload;
mod query; mod query;
pub(crate) mod readlines; mod readlines;
pub use self::either::{Either, EitherExtractError}; pub use self::either::Either;
pub use self::form::{Form, FormConfig}; pub use self::form::{Form, FormConfig, UrlEncoded};
pub use self::header::Header; pub use self::header::Header;
pub use self::json::{Json, JsonConfig}; pub use self::json::{Json, JsonBody, JsonConfig};
pub use self::path::{Path, PathConfig}; pub use self::path::{Path, PathConfig};
pub use self::payload::{Payload, PayloadConfig}; pub use self::payload::{Payload, PayloadConfig};
pub use self::query::{Query, QueryConfig}; pub use self::query::{Query, QueryConfig};

View file

@ -248,6 +248,7 @@ impl PayloadConfig {
} }
} }
} }
Ok(()) Ok(())
} }

View file

@ -2,13 +2,12 @@
use std::future::Future; use std::future::Future;
use actix_http::Method;
use actix_router::IntoPatterns; use actix_router::IntoPatterns;
pub use bytes::{Buf, BufMut, Bytes, BytesMut}; pub use bytes::{Buf, BufMut, Bytes, BytesMut};
use crate::{ use crate::{
error::BlockingError, extract::FromRequest, handler::Handler, resource::Resource, error::BlockingError, http::Method, service::WebService, FromRequest, Handler, Resource,
route::Route, scope::Scope, service::WebService, Responder, Responder, Route, Scope,
}; };
pub use crate::config::ServiceConfig; pub use crate::config::ServiceConfig;

313
tests/compression.rs Normal file
View file

@ -0,0 +1,313 @@
use actix_http::ContentEncoding;
use actix_web::{
dev::BodyEncoding as _,
http::{header, StatusCode},
middleware::Compress,
web, App, HttpResponse,
};
use bytes::Bytes;
mod test_utils;
use test_utils::{brotli, gzip, zstd};
static LOREM: &[u8] = include_bytes!("fixtures/lorem.txt");
static LOREM_GZIP: &[u8] = include_bytes!("fixtures/lorem.txt.gz");
static LOREM_BR: &[u8] = include_bytes!("fixtures/lorem.txt.br");
static LOREM_ZSTD: &[u8] = include_bytes!("fixtures/lorem.txt.zst");
static LOREM_XZ: &[u8] = include_bytes!("fixtures/lorem.txt.xz");
macro_rules! test_server {
() => {
actix_test::start(|| {
App::new()
.wrap(Compress::default())
.route("/static", web::to(|| HttpResponse::Ok().body(LOREM)))
.route(
"/static-gzip",
web::to(|| {
HttpResponse::Ok()
// signal to compressor that content should not be altered
.encode_with(ContentEncoding::Identity)
// signal to client that content is encoded
.insert_header(ContentEncoding::Gzip)
.body(LOREM_GZIP)
}),
)
.route(
"/static-br",
web::to(|| {
HttpResponse::Ok()
// signal to compressor that content should not be altered
.encode_with(ContentEncoding::Identity)
// signal to client that content is encoded
.insert_header(ContentEncoding::Brotli)
.body(LOREM_BR)
}),
)
.route(
"/static-zstd",
web::to(|| {
HttpResponse::Ok()
// signal to compressor that content should not be altered
.encode_with(ContentEncoding::Identity)
// signal to client that content is encoded
.insert_header(ContentEncoding::Zstd)
.body(LOREM_ZSTD)
}),
)
.route(
"/static-xz",
web::to(|| {
HttpResponse::Ok()
// signal to compressor that content should not be altered
.encode_with(ContentEncoding::Identity)
// signal to client that content is encoded as 7zip
.insert_header((header::CONTENT_ENCODING, "xz"))
.body(LOREM_XZ)
}),
)
.route(
"/echo",
web::to(|body: Bytes| HttpResponse::Ok().body(body)),
)
})
};
}
#[actix_rt::test]
async fn negotiate_encoding_identity() {
let srv = test_server!();
let req = srv
.post("/static")
.insert_header((header::ACCEPT_ENCODING, "identity"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING), None);
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
srv.stop().await;
}
#[actix_rt::test]
async fn negotiate_encoding_gzip() {
let srv = test_server!();
let req = srv
.post("/static")
.insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
let mut res = srv
.post("/static")
.no_decompress()
.insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd"))
.send()
.await
.unwrap();
let bytes = res.body().await.unwrap();
assert_eq!(gzip::decode(bytes), LOREM);
srv.stop().await;
}
#[actix_rt::test]
async fn negotiate_encoding_br() {
let srv = test_server!();
let req = srv
.post("/static")
.insert_header((header::ACCEPT_ENCODING, "br,zstd,gzip"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
let mut res = srv
.post("/static")
.no_decompress()
.insert_header((header::ACCEPT_ENCODING, "br,zstd,gzip"))
.send()
.await
.unwrap();
let bytes = res.body().await.unwrap();
assert_eq!(brotli::decode(bytes), LOREM);
srv.stop().await;
}
#[actix_rt::test]
async fn negotiate_encoding_zstd() {
let srv = test_server!();
let req = srv
.post("/static")
.insert_header((header::ACCEPT_ENCODING, "zstd,gzip,br"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "zstd");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
let mut res = srv
.post("/static")
.no_decompress()
.insert_header((header::ACCEPT_ENCODING, "zstd,gzip,br"))
.send()
.await
.unwrap();
let bytes = res.body().await.unwrap();
assert_eq!(zstd::decode(bytes), LOREM);
srv.stop().await;
}
#[cfg(all(
feature = "compress-brotli",
feature = "compress-gzip",
feature = "compress-zstd",
))]
#[actix_rt::test]
async fn client_encoding_prefers_brotli() {
let srv = test_server!();
let req = srv.post("/static").send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
srv.stop().await;
}
#[actix_rt::test]
async fn gzip_no_decompress() {
let srv = test_server!();
let req = srv
.post("/static-gzip")
// don't decompress response body
.no_decompress()
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM_GZIP));
srv.stop().await;
}
#[actix_rt::test]
async fn manual_custom_coding() {
let srv = test_server!();
let req = srv
.post("/static-xz")
// don't decompress response body
.no_decompress()
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "xz"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "xz");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM_XZ));
srv.stop().await;
}
#[actix_rt::test]
async fn deny_identity_coding() {
let srv = test_server!();
let req = srv
.post("/static")
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "br, identity;q=0"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
srv.stop().await;
}
#[actix_rt::test]
async fn deny_identity_coding_no_decompress() {
let srv = test_server!();
let req = srv
.post("/static-br")
// don't decompress response body
.no_decompress()
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "br, identity;q=0"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM_BR));
srv.stop().await;
}
// TODO: fix test
// currently fails because negotiation doesn't consider unknown encoding types
#[ignore]
#[actix_rt::test]
async fn deny_identity_for_manual_coding() {
let srv = test_server!();
let req = srv
.post("/static-xz")
// don't decompress response body
.no_decompress()
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "xz, identity;q=0"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "xz");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM_XZ));
srv.stop().await;
}

5
tests/fixtures/lorem.txt vendored Normal file
View file

@ -0,0 +1,5 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin interdum tincidunt lacus, sed tempor lorem consectetur et. Pellentesque et egestas sem, at cursus massa. Nunc feugiat elit sit amet ipsum commodo luctus. Proin auctor dignissim pharetra. Integer iaculis quam a tellus auctor, vitae auctor nisl viverra. Nullam consequat maximus porttitor. Pellentesque tortor enim, molestie at varius non, tempor non nibh. Suspendisse tempus erat lorem, vel faucibus magna blandit vel. Sed pellentesque ligula augue, vitae fermentum eros blandit et. Cras dignissim in massa ut varius. Vestibulum commodo nunc sit amet pellentesque dignissim.
Donec imperdiet blandit lobortis. Suspendisse fringilla nunc quis venenatis tempor. Nunc tempor sed erat sed convallis. Pellentesque aliquet elit lectus, quis vulputate arcu pharetra sed. Etiam laoreet aliquet arcu cursus vehicula. Maecenas odio odio, elementum faucibus sollicitudin vitae, pellentesque ac purus. Donec venenatis faucibus lorem, et finibus lacus tincidunt vitae. Quisque laoreet metus sapien, vitae euismod mauris lobortis malesuada. Integer sit amet elementum turpis. Maecenas ex mauris, dapibus eu placerat vitae, rutrum convallis enim. Nulla vitae orci ultricies, sagittis turpis et, lacinia dui. Praesent egestas urna turpis, sit amet feugiat mauris tristique eu. Quisque id tempor libero. Donec ullamcorper dapibus lorem, vel consequat risus congue a.
Nullam dignissim ut lectus vitae tempor. Pellentesque ut odio fringilla, volutpat mi et, vulputate tellus. Fusce eget diam non odio tincidunt viverra eu vel augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam sed eleifend purus, vitae aliquam orci. Cras fringilla justo eget tempus bibendum. Phasellus venenatis, odio nec pulvinar commodo, quam neque lacinia turpis, ut rutrum tortor massa eu nulla. Vivamus tincidunt ut lectus a gravida. Donec varius mi quis enim interdum ultrices. Sed aliquam consectetur nisi vitae viverra. Praesent nec ligula egestas, porta lectus sed, consectetur augue.

BIN
tests/fixtures/lorem.txt.br vendored Normal file

Binary file not shown.

BIN
tests/fixtures/lorem.txt.gz vendored Normal file

Binary file not shown.

BIN
tests/fixtures/lorem.txt.xz vendored Normal file

Binary file not shown.

BIN
tests/fixtures/lorem.txt.zst vendored Normal file

Binary file not shown.

View file

@ -11,30 +11,28 @@ use std::{
}; };
use actix_web::{ use actix_web::{
cookie::{Cookie, CookieBuilder},
dev::BodyEncoding, dev::BodyEncoding,
http::header::{ http::{
ContentEncoding, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING, header::{self, ContentEncoding, ACCEPT_ENCODING, CONTENT_ENCODING, TRANSFER_ENCODING},
StatusCode,
}, },
middleware::{Compress, NormalizePath, TrailingSlash}, middleware::{Compress, NormalizePath, TrailingSlash},
web, App, Error, HttpResponse, web, App, Error, HttpResponse,
}; };
use brotli2::write::{BrotliDecoder, BrotliEncoder};
use bytes::Bytes; use bytes::Bytes;
use cookie::{Cookie, CookieBuilder};
use flate2::{
read::GzDecoder,
write::{GzEncoder, ZlibDecoder, ZlibEncoder},
Compression,
};
use futures_core::ready; use futures_core::ready;
use rand::{distributions::Alphanumeric, Rng as _};
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
use openssl::{ use openssl::{
pkey::PKey, pkey::PKey,
ssl::{SslAcceptor, SslMethod}, ssl::{SslAcceptor, SslMethod},
x509::X509, x509::X509,
}; };
use rand::{distributions::Alphanumeric, Rng};
use zstd::stream::{read::Decoder as ZstdDecoder, write::Encoder as ZstdEncoder}; mod test_utils;
use test_utils::{brotli, deflate, gzip, zstd};
const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \
@ -122,165 +120,85 @@ async fn test_body() {
App::new().service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) App::new().service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))))
}); });
let mut response = srv.get("/").send().await.unwrap(); let mut res = srv.get("/").send().await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await; srv.stop().await;
} }
#[actix_rt::test]
async fn test_body_gzip() {
let srv = actix_test::start_with(actix_test::config().h1(), || {
App::new()
.wrap(Compress::new(ContentEncoding::Gzip))
.service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))))
});
let mut response = srv
.get("/")
.no_decompress()
.append_header((ACCEPT_ENCODING, "gzip"))
.send()
.await
.unwrap();
assert!(response.status().is_success());
// read response
let bytes = response.body().await.unwrap();
// decode
let mut e = GzDecoder::new(&bytes[..]);
let mut dec = Vec::new();
e.read_to_end(&mut dec).unwrap();
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
srv.stop().await;
}
#[actix_rt::test]
async fn test_body_gzip2() {
let srv = actix_test::start_with(actix_test::config().h1(), || {
App::new()
.wrap(Compress::new(ContentEncoding::Gzip))
.service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))))
});
let mut response = srv
.get("/")
.no_decompress()
.append_header((ACCEPT_ENCODING, "gzip"))
.send()
.await
.unwrap();
assert!(response.status().is_success());
// read response
let bytes = response.body().await.unwrap();
// decode
let mut e = GzDecoder::new(&bytes[..]);
let mut dec = Vec::new();
e.read_to_end(&mut dec).unwrap();
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
srv.stop().await;
}
#[actix_rt::test] #[actix_rt::test]
async fn test_body_encoding_override() { async fn test_body_encoding_override() {
let srv = actix_test::start_with(actix_test::config().h1(), || { let srv = actix_test::start_with(actix_test::config().h1(), || {
App::new() App::new()
.wrap(Compress::new(ContentEncoding::Gzip)) .wrap(Compress::default())
.service(web::resource("/").route(web::to(|| { .service(web::resource("/").route(web::to(|| {
HttpResponse::Ok() HttpResponse::Ok()
.encoding(ContentEncoding::Deflate) .encode_with(ContentEncoding::Deflate)
.body(STR) .body(STR)
}))) })))
.service(web::resource("/raw").route(web::to(|| { .service(web::resource("/raw").route(web::to(|| {
let mut response = let mut res = HttpResponse::with_body(actix_web::http::StatusCode::OK, STR);
HttpResponse::with_body(actix_web::http::StatusCode::OK, STR); res.encode_with(ContentEncoding::Deflate);
response.encoding(ContentEncoding::Deflate); res.map_into_boxed_body()
response.map_into_boxed_body()
}))) })))
}); });
// Builder // Builder
let mut response = srv let mut res = srv
.get("/") .get("/")
.no_decompress() .no_decompress()
.append_header((ACCEPT_ENCODING, "deflate")) .append_header((ACCEPT_ENCODING, "deflate"))
.send() .send()
.await .await
.unwrap(); .unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap(); assert_eq!(deflate::decode(bytes), STR.as_bytes());
// decode
let mut e = ZlibDecoder::new(Vec::new());
e.write_all(bytes.as_ref()).unwrap();
let dec = e.finish().unwrap();
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
// Raw Response // Raw Response
let mut response = srv let mut res = srv
.request(actix_web::http::Method::GET, srv.url("/raw")) .request(actix_web::http::Method::GET, srv.url("/raw"))
.no_decompress() .no_decompress()
.append_header((ACCEPT_ENCODING, "deflate")) .append_header((ACCEPT_ENCODING, "deflate"))
.send() .send()
.await .await
.unwrap(); .unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap(); assert_eq!(deflate::decode(bytes), STR.as_bytes());
// decode
let mut e = ZlibDecoder::new(Vec::new());
e.write_all(bytes.as_ref()).unwrap();
let dec = e.finish().unwrap();
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
srv.stop().await; srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_body_gzip_large() { async fn body_gzip_large() {
let data = STR.repeat(10); let data = STR.repeat(10);
let srv_data = data.clone(); let srv_data = data.clone();
let srv = actix_test::start_with(actix_test::config().h1(), move || { let srv = actix_test::start_with(actix_test::config().h1(), move || {
let data = srv_data.clone(); let data = srv_data.clone();
App::new()
.wrap(Compress::new(ContentEncoding::Gzip)) App::new().wrap(Compress::default()).service(
.service( web::resource("/").route(web::to(move || HttpResponse::Ok().body(data.clone()))),
web::resource("/") )
.route(web::to(move || HttpResponse::Ok().body(data.clone()))),
)
}); });
let mut response = srv let mut res = srv
.get("/") .get("/")
.no_decompress() .no_decompress()
.append_header((ACCEPT_ENCODING, "gzip")) .append_header((ACCEPT_ENCODING, "gzip"))
.send() .send()
.await .await
.unwrap(); .unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap(); assert_eq!(gzip::decode(bytes), data.as_bytes());
// decode
let mut e = GzDecoder::new(&bytes[..]);
let mut dec = Vec::new();
e.read_to_end(&mut dec).unwrap();
assert_eq!(Bytes::from(dec), Bytes::from(data));
srv.stop().await; srv.stop().await;
} }
@ -296,32 +214,22 @@ async fn test_body_gzip_large_random() {
let srv = actix_test::start_with(actix_test::config().h1(), move || { let srv = actix_test::start_with(actix_test::config().h1(), move || {
let data = srv_data.clone(); let data = srv_data.clone();
App::new() App::new().wrap(Compress::default()).service(
.wrap(Compress::new(ContentEncoding::Gzip)) web::resource("/").route(web::to(move || HttpResponse::Ok().body(data.clone()))),
.service( )
web::resource("/")
.route(web::to(move || HttpResponse::Ok().body(data.clone()))),
)
}); });
let mut response = srv let mut res = srv
.get("/") .get("/")
.no_decompress() .no_decompress()
.append_header((ACCEPT_ENCODING, "gzip")) .append_header((ACCEPT_ENCODING, "gzip"))
.send() .send()
.await .await
.unwrap(); .unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap(); assert_eq!(gzip::decode(bytes), data.as_bytes());
// decode
let mut e = GzDecoder::new(&bytes[..]);
let mut dec = Vec::new();
e.read_to_end(&mut dec).unwrap();
assert_eq!(dec.len(), data.len());
assert_eq!(Bytes::from(dec), Bytes::from(data));
srv.stop().await; srv.stop().await;
} }
@ -330,34 +238,25 @@ async fn test_body_gzip_large_random() {
async fn test_body_chunked_implicit() { async fn test_body_chunked_implicit() {
let srv = actix_test::start_with(actix_test::config().h1(), || { let srv = actix_test::start_with(actix_test::config().h1(), || {
App::new() App::new()
.wrap(Compress::new(ContentEncoding::Gzip)) .wrap(Compress::default())
.service(web::resource("/").route(web::get().to(move || { .service(web::resource("/").route(web::get().to(move || {
HttpResponse::Ok() HttpResponse::Ok()
.streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) .streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24))
}))) })))
}); });
let mut response = srv let mut res = srv
.get("/") .get("/")
.no_decompress() .no_decompress()
.append_header((ACCEPT_ENCODING, "gzip")) .append_header((ACCEPT_ENCODING, "gzip"))
.send() .send()
.await .await
.unwrap(); .unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
assert_eq!( assert_eq!(res.headers().get(TRANSFER_ENCODING).unwrap(), "chunked");
response.headers().get(TRANSFER_ENCODING).unwrap(),
&b"chunked"[..]
);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap(); assert_eq!(gzip::decode(bytes), STR.as_bytes());
// decode
let mut e = GzDecoder::new(&bytes[..]);
let mut dec = Vec::new();
e.read_to_end(&mut dec).unwrap();
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
srv.stop().await; srv.stop().await;
} }
@ -366,32 +265,24 @@ async fn test_body_chunked_implicit() {
async fn test_body_br_streaming() { async fn test_body_br_streaming() {
let srv = actix_test::start_with(actix_test::config().h1(), || { let srv = actix_test::start_with(actix_test::config().h1(), || {
App::new() App::new()
.wrap(Compress::new(ContentEncoding::Br)) .wrap(Compress::default())
.service(web::resource("/").route(web::to(move || { .service(web::resource("/").route(web::to(move || {
HttpResponse::Ok() HttpResponse::Ok()
.streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) .streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24))
}))) })))
}); });
let mut response = srv let mut res = srv
.get("/") .get("/")
.append_header((ACCEPT_ENCODING, "br")) .append_header((ACCEPT_ENCODING, "br"))
.no_decompress() .no_decompress()
.send() .send()
.await .await
.unwrap(); .unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap(); assert_eq!(brotli::decode(bytes), STR.as_bytes());
println!("TEST: {:?}", bytes.len());
// decode br
let mut e = BrotliDecoder::new(Vec::with_capacity(2048));
e.write_all(bytes.as_ref()).unwrap();
let dec = e.finish().unwrap();
println!("T: {:?}", Bytes::copy_from_slice(&dec));
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
srv.stop().await; srv.stop().await;
} }
@ -404,16 +295,13 @@ async fn test_head_binary() {
) )
}); });
let mut response = srv.head("/").send().await.unwrap(); let mut res = srv.head("/").send().await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
{ let len = res.headers().get(header::CONTENT_LENGTH).unwrap();
let len = response.headers().get(CONTENT_LENGTH).unwrap(); assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
}
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap();
assert!(bytes.is_empty()); assert!(bytes.is_empty());
srv.stop().await; srv.stop().await;
@ -429,12 +317,11 @@ async fn test_no_chunking() {
}))) })))
}); });
let mut response = srv.get("/").send().await.unwrap(); let mut res = srv.get("/").send().await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
assert!(!response.headers().contains_key(TRANSFER_ENCODING)); assert!(!res.headers().contains_key(TRANSFER_ENCODING));
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await; srv.stop().await;
@ -444,27 +331,21 @@ async fn test_no_chunking() {
async fn test_body_deflate() { async fn test_body_deflate() {
let srv = actix_test::start_with(actix_test::config().h1(), || { let srv = actix_test::start_with(actix_test::config().h1(), || {
App::new() App::new()
.wrap(Compress::new(ContentEncoding::Deflate)) .wrap(Compress::default())
.service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR)))) .service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR))))
}); });
// client request let mut res = srv
let mut response = srv
.get("/") .get("/")
.append_header((ACCEPT_ENCODING, "deflate")) .append_header((ACCEPT_ENCODING, "deflate"))
.no_decompress() .no_decompress()
.send() .send()
.await .await
.unwrap(); .unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap(); assert_eq!(deflate::decode(bytes), STR.as_bytes());
let mut e = ZlibDecoder::new(Vec::new());
e.write_all(bytes.as_ref()).unwrap();
let dec = e.finish().unwrap();
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
srv.stop().await; srv.stop().await;
} }
@ -473,28 +354,21 @@ async fn test_body_deflate() {
async fn test_body_brotli() { async fn test_body_brotli() {
let srv = actix_test::start_with(actix_test::config().h1(), || { let srv = actix_test::start_with(actix_test::config().h1(), || {
App::new() App::new()
.wrap(Compress::new(ContentEncoding::Br)) .wrap(Compress::default())
.service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR)))) .service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR))))
}); });
// client request let mut res = srv
let mut response = srv
.get("/") .get("/")
.append_header((ACCEPT_ENCODING, "br")) .append_header((ACCEPT_ENCODING, "br"))
.no_decompress() .no_decompress()
.send() .send()
.await .await
.unwrap(); .unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap(); assert_eq!(brotli::decode(bytes), STR.as_bytes());
// decode brotli
let mut e = BrotliDecoder::new(Vec::with_capacity(2048));
e.write_all(bytes.as_ref()).unwrap();
let dec = e.finish().unwrap();
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
srv.stop().await; srv.stop().await;
} }
@ -503,28 +377,21 @@ async fn test_body_brotli() {
async fn test_body_zstd() { async fn test_body_zstd() {
let srv = actix_test::start_with(actix_test::config().h1(), || { let srv = actix_test::start_with(actix_test::config().h1(), || {
App::new() App::new()
.wrap(Compress::new(ContentEncoding::Zstd)) .wrap(Compress::default())
.service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR)))) .service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR))))
}); });
// client request let mut res = srv
let mut response = srv
.get("/") .get("/")
.append_header((ACCEPT_ENCODING, "zstd")) .append_header((ACCEPT_ENCODING, "zstd"))
.no_decompress() .no_decompress()
.send() .send()
.await .await
.unwrap(); .unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap(); assert_eq!(zstd::decode(bytes), STR.as_bytes());
// decode
let mut e = ZstdDecoder::new(&bytes[..]).unwrap();
let mut dec = Vec::new();
e.read_to_end(&mut dec).unwrap();
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
srv.stop().await; srv.stop().await;
} }
@ -533,31 +400,24 @@ async fn test_body_zstd() {
async fn test_body_zstd_streaming() { async fn test_body_zstd_streaming() {
let srv = actix_test::start_with(actix_test::config().h1(), || { let srv = actix_test::start_with(actix_test::config().h1(), || {
App::new() App::new()
.wrap(Compress::new(ContentEncoding::Zstd)) .wrap(Compress::default())
.service(web::resource("/").route(web::to(move || { .service(web::resource("/").route(web::to(move || {
HttpResponse::Ok() HttpResponse::Ok()
.streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) .streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24))
}))) })))
}); });
// client request let mut res = srv
let mut response = srv
.get("/") .get("/")
.append_header((ACCEPT_ENCODING, "zstd")) .append_header((ACCEPT_ENCODING, "zstd"))
.no_decompress() .no_decompress()
.send() .send()
.await .await
.unwrap(); .unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap(); assert_eq!(zstd::decode(bytes), STR.as_bytes());
// decode
let mut e = ZstdDecoder::new(&bytes[..]).unwrap();
let mut dec = Vec::new();
e.read_to_end(&mut dec).unwrap();
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
srv.stop().await; srv.stop().await;
} }
@ -570,20 +430,14 @@ async fn test_zstd_encoding() {
) )
}); });
let mut e = ZstdEncoder::new(Vec::new(), 5).unwrap();
e.write_all(STR.as_ref()).unwrap();
let enc = e.finish().unwrap();
// client request
let request = srv let request = srv
.post("/") .post("/")
.append_header((CONTENT_ENCODING, "zstd")) .append_header((CONTENT_ENCODING, "zstd"))
.send_body(enc.clone()); .send_body(zstd::encode(STR));
let mut response = request.await.unwrap(); let mut res = request.await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await; srv.stop().await;
@ -607,21 +461,15 @@ async fn test_zstd_encoding_large() {
) )
}); });
let mut e = ZstdEncoder::new(Vec::new(), 5).unwrap();
e.write_all(data.as_ref()).unwrap();
let enc = e.finish().unwrap();
// client request
let request = srv let request = srv
.post("/") .post("/")
.append_header((CONTENT_ENCODING, "zstd")) .append_header((CONTENT_ENCODING, "zstd"))
.send_body(enc.clone()); .send_body(zstd::encode(&data));
let mut response = request.await.unwrap(); let mut res = request.await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().limit(320_000).await.unwrap();
let bytes = response.body().limit(320_000).await.unwrap(); assert_eq!(bytes, data.as_bytes());
assert_eq!(bytes, Bytes::from(data));
srv.stop().await; srv.stop().await;
} }
@ -634,20 +482,14 @@ async fn test_encoding() {
) )
}); });
// client request
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(STR.as_ref()).unwrap();
let enc = e.finish().unwrap();
let request = srv let request = srv
.post("/") .post("/")
.insert_header((CONTENT_ENCODING, "gzip")) .insert_header((CONTENT_ENCODING, "gzip"))
.send_body(enc.clone()); .send_body(gzip::encode(STR));
let mut response = request.await.unwrap(); let mut res = request.await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await; srv.stop().await;
@ -661,21 +503,15 @@ async fn test_gzip_encoding() {
) )
}); });
// client request
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(STR.as_ref()).unwrap();
let enc = e.finish().unwrap();
let request = srv let request = srv
.post("/") .post("/")
.append_header((CONTENT_ENCODING, "gzip")) .append_header((CONTENT_ENCODING, "gzip"))
.send_body(enc.clone()); .send_body(gzip::encode(STR));
let mut response = request.await.unwrap(); let mut res = request.await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap(); assert_eq!(bytes, STR.as_bytes());
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await; srv.stop().await;
} }
@ -689,21 +525,15 @@ async fn test_gzip_encoding_large() {
) )
}); });
// client request let req = srv
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(data.as_ref()).unwrap();
let enc = e.finish().unwrap();
let request = srv
.post("/") .post("/")
.append_header((CONTENT_ENCODING, "gzip")) .append_header((CONTENT_ENCODING, "gzip"))
.send_body(enc.clone()); .send_body(gzip::encode(&data));
let mut response = request.await.unwrap(); let mut res = req.await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap(); assert_eq!(bytes, data);
assert_eq!(bytes, Bytes::from(data));
srv.stop().await; srv.stop().await;
} }
@ -722,22 +552,15 @@ async fn test_reading_gzip_encoding_large_random() {
) )
}); });
// client request
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(data.as_ref()).unwrap();
let enc = e.finish().unwrap();
let request = srv let request = srv
.post("/") .post("/")
.append_header((CONTENT_ENCODING, "gzip")) .append_header((CONTENT_ENCODING, "gzip"))
.send_body(enc.clone()); .send_body(gzip::encode(&data));
let mut response = request.await.unwrap(); let mut res = request.await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap(); assert_eq!(bytes, data.as_bytes());
assert_eq!(bytes.len(), data.len());
assert_eq!(bytes, Bytes::from(data));
srv.stop().await; srv.stop().await;
} }
@ -750,20 +573,14 @@ async fn test_reading_deflate_encoding() {
) )
}); });
let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
e.write_all(STR.as_ref()).unwrap();
let enc = e.finish().unwrap();
// client request
let request = srv let request = srv
.post("/") .post("/")
.append_header((CONTENT_ENCODING, "deflate")) .append_header((CONTENT_ENCODING, "deflate"))
.send_body(enc.clone()); .send_body(deflate::encode(STR));
let mut response = request.await.unwrap(); let mut res = request.await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await; srv.stop().await;
@ -778,20 +595,14 @@ async fn test_reading_deflate_encoding_large() {
) )
}); });
let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
e.write_all(data.as_ref()).unwrap();
let enc = e.finish().unwrap();
// client request
let request = srv let request = srv
.post("/") .post("/")
.append_header((CONTENT_ENCODING, "deflate")) .append_header((CONTENT_ENCODING, "deflate"))
.send_body(enc.clone()); .send_body(deflate::encode(&data));
let mut response = request.await.unwrap(); let mut res = request.await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap();
assert_eq!(bytes, Bytes::from(data)); assert_eq!(bytes, Bytes::from(data));
srv.stop().await; srv.stop().await;
@ -811,20 +622,14 @@ async fn test_reading_deflate_encoding_large_random() {
) )
}); });
let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
e.write_all(data.as_ref()).unwrap();
let enc = e.finish().unwrap();
// client request
let request = srv let request = srv
.post("/") .post("/")
.append_header((CONTENT_ENCODING, "deflate")) .append_header((CONTENT_ENCODING, "deflate"))
.send_body(enc.clone()); .send_body(deflate::encode(&data));
let mut response = request.await.unwrap(); let mut res = request.await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap();
assert_eq!(bytes.len(), data.len()); assert_eq!(bytes.len(), data.len());
assert_eq!(bytes, Bytes::from(data)); assert_eq!(bytes, Bytes::from(data));
@ -839,20 +644,14 @@ async fn test_brotli_encoding() {
) )
}); });
let mut e = BrotliEncoder::new(Vec::new(), 5);
e.write_all(STR.as_ref()).unwrap();
let enc = e.finish().unwrap();
// client request
let request = srv let request = srv
.post("/") .post("/")
.append_header((CONTENT_ENCODING, "br")) .append_header((CONTENT_ENCODING, "br"))
.send_body(enc.clone()); .send_body(brotli::encode(STR));
let mut response = request.await.unwrap(); let mut res = request.await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await; srv.stop().await;
@ -876,20 +675,14 @@ async fn test_brotli_encoding_large() {
) )
}); });
let mut e = BrotliEncoder::new(Vec::new(), 5);
e.write_all(data.as_ref()).unwrap();
let enc = e.finish().unwrap();
// client request
let request = srv let request = srv
.post("/") .post("/")
.append_header((CONTENT_ENCODING, "br")) .append_header((CONTENT_ENCODING, "br"))
.send_body(enc.clone()); .send_body(brotli::encode(&data));
let mut response = request.await.unwrap(); let mut res = request.await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().limit(320_000).await.unwrap();
let bytes = response.body().limit(320_000).await.unwrap();
assert_eq!(bytes, Bytes::from(data)); assert_eq!(bytes, Bytes::from(data));
srv.stop().await; srv.stop().await;
@ -898,32 +691,27 @@ async fn test_brotli_encoding_large() {
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
#[actix_rt::test] #[actix_rt::test]
async fn test_brotli_encoding_large_openssl() { async fn test_brotli_encoding_large_openssl() {
use actix_web::http::header;
let data = STR.repeat(10); let data = STR.repeat(10);
let srv = let srv =
actix_test::start_with(actix_test::config().openssl(openssl_config()), move || { actix_test::start_with(actix_test::config().openssl(openssl_config()), move || {
App::new().service(web::resource("/").route(web::to(|bytes: Bytes| { App::new().service(web::resource("/").route(web::to(|bytes: Bytes| {
HttpResponse::Ok() HttpResponse::Ok()
.encoding(ContentEncoding::Identity) .encode_with(ContentEncoding::Identity)
.body(bytes) .body(bytes)
}))) })))
}); });
// body let mut res = srv
let mut enc = BrotliEncoder::new(Vec::new(), 3);
enc.write_all(data.as_ref()).unwrap();
let enc = enc.finish().unwrap();
// client request
let mut response = srv
.post("/") .post("/")
.append_header((actix_web::http::header::CONTENT_ENCODING, "br")) .append_header((header::CONTENT_ENCODING, "br"))
.send_body(enc) .send_body(brotli::encode(&data))
.await .await
.unwrap(); .unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap();
assert_eq!(bytes, Bytes::from(data)); assert_eq!(bytes, Bytes::from(data));
srv.stop().await; srv.stop().await;
@ -971,27 +759,20 @@ mod plus_rustls {
let srv = actix_test::start_with(actix_test::config().rustls(tls_config()), || { let srv = actix_test::start_with(actix_test::config().rustls(tls_config()), || {
App::new().service(web::resource("/").route(web::to(|bytes: Bytes| { App::new().service(web::resource("/").route(web::to(|bytes: Bytes| {
HttpResponse::Ok() HttpResponse::Ok()
.encoding(ContentEncoding::Identity) .encode_with(ContentEncoding::Identity)
.body(bytes) .body(bytes)
}))) })))
}); });
// encode data
let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
e.write_all(data.as_ref()).unwrap();
let enc = e.finish().unwrap();
// client request
let req = srv let req = srv
.post("/") .post("/")
.insert_header((actix_web::http::header::CONTENT_ENCODING, "deflate")) .insert_header((actix_web::http::header::CONTENT_ENCODING, "deflate"))
.send_stream(TestBody::new(Bytes::from(enc), 1024)); .send_stream(TestBody::new(Bytes::from(deflate::encode(&data)), 1024));
let mut response = req.await.unwrap(); let mut res = req.await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
// read response let bytes = res.body().await.unwrap();
let bytes = response.body().await.unwrap();
assert_eq!(bytes.len(), data.len()); assert_eq!(bytes.len(), data.len());
assert_eq!(bytes, Bytes::from(data)); assert_eq!(bytes, Bytes::from(data));
@ -1084,8 +865,8 @@ async fn test_normalize() {
.service(web::resource("/one").route(web::to(HttpResponse::Ok))) .service(web::resource("/one").route(web::to(HttpResponse::Ok)))
}); });
let response = srv.get("/one/").send().await.unwrap(); let res = srv.get("/one/").send().await.unwrap();
assert!(response.status().is_success()); assert_eq!(res.status(), StatusCode::OK);
srv.stop().await srv.stop().await
} }
@ -1148,15 +929,20 @@ async fn test_accept_encoding_no_match() {
.service(web::resource("/").route(web::to(move || HttpResponse::Ok().finish()))) .service(web::resource("/").route(web::to(move || HttpResponse::Ok().finish())))
}); });
let response = srv let mut res = srv
.get("/") .get("/")
.append_header((ACCEPT_ENCODING, "compress, identity;q=0")) .insert_header((ACCEPT_ENCODING, "xz, identity;q=0"))
.no_decompress() .no_decompress()
.send() .send()
.await .await
.unwrap(); .unwrap();
assert_eq!(response.status().as_u16(), 406); assert_eq!(res.status(), StatusCode::NOT_ACCEPTABLE);
assert_eq!(res.headers().get(CONTENT_ENCODING), None);
let bytes = res.body().await.unwrap();
// body should contain the supported encodings
assert!(!bytes.is_empty());
srv.stop().await; srv.stop().await;
} }

76
tests/test_utils.rs Normal file
View file

@ -0,0 +1,76 @@
// compiling some tests will trigger unused function warnings even though other tests use them
#![allow(dead_code)]
use std::io::{Read as _, Write as _};
pub mod gzip {
use super::*;
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(bytes.as_ref()).unwrap();
encoder.finish().unwrap()
}
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut decoder = GzDecoder::new(bytes.as_ref());
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf
}
}
pub mod deflate {
use super::*;
use flate2::{read::ZlibDecoder, write::ZlibEncoder, Compression};
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(bytes.as_ref()).unwrap();
encoder.finish().unwrap()
}
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut decoder = ZlibDecoder::new(bytes.as_ref());
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf
}
}
pub mod brotli {
use super::*;
use ::brotli2::{read::BrotliDecoder, write::BrotliEncoder};
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut encoder = BrotliEncoder::new(Vec::new(), 3);
encoder.write_all(bytes.as_ref()).unwrap();
encoder.finish().unwrap()
}
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut decoder = BrotliDecoder::new(bytes.as_ref());
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf
}
}
pub mod zstd {
use super::*;
use ::zstd::stream::{read::Decoder, write::Encoder};
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut encoder = Encoder::new(Vec::new(), 3).unwrap();
encoder.write_all(bytes.as_ref()).unwrap();
encoder.finish().unwrap()
}
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
let mut decoder = Decoder::new(bytes.as_ref()).unwrap();
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf
}
}