mirror of https://github.com/fafhrd91/actix-web
use AcceptEncoding header in Compress middleware
This commit is contained in:
parent
a53479eee6
commit
8f01baa7ed
|
@ -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();
|
||||||
|
|
|
@ -27,7 +27,7 @@ use super::Writer;
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{self, BodySize, MessageBody},
|
body::{self, BodySize, MessageBody},
|
||||||
error::BlockingError,
|
error::BlockingError,
|
||||||
header::{self, ContentEncoding, HeaderValue, CONTENT_ENCODING},
|
header::{self, ContentEncoding, CONTENT_ENCODING},
|
||||||
ResponseHead, StatusCode,
|
ResponseHead, StatusCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,8 +59,7 @@ impl<B: MessageBody> Encoder<B> {
|
||||||
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|
let can_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) {
|
||||||
|
@ -252,10 +251,8 @@ 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.no_chunking(false);
|
head.no_chunking(false);
|
||||||
}
|
}
|
||||||
|
@ -268,7 +265,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 +289,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")]
|
||||||
|
@ -310,7 +307,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 +323,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 +351,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);
|
||||||
|
|
|
@ -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`]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -23,11 +23,13 @@ pub struct ContentEncodingParseError;
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
#[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,36 @@ 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?
|
// /// Is the content compressed?
|
||||||
#[inline]
|
// #[inline]
|
||||||
pub const fn is_compression(self) -> bool {
|
// pub const fn is_compression(self) -> bool {
|
||||||
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
|
// matches!(self, ContentEncoding::Identity)
|
||||||
}
|
// }
|
||||||
|
|
||||||
/// 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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,7 +86,7 @@ impl FromStr for ContentEncoding {
|
||||||
let val = val.trim();
|
let val = val.trim();
|
||||||
|
|
||||||
if val.eq_ignore_ascii_case("br") {
|
if val.eq_ignore_ascii_case("br") {
|
||||||
Ok(ContentEncoding::Br)
|
Ok(ContentEncoding::Brotli)
|
||||||
} else if val.eq_ignore_ascii_case("gzip") {
|
} else if val.eq_ignore_ascii_case("gzip") {
|
||||||
Ok(ContentEncoding::Gzip)
|
Ok(ContentEncoding::Gzip)
|
||||||
} else if val.eq_ignore_ascii_case("deflate") {
|
} else if val.eq_ignore_ascii_case("deflate") {
|
||||||
|
|
|
@ -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
|
||||||
|
@ -157,8 +158,11 @@ 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.0001);
|
||||||
/// assert_eq!(q2, Quality::MIN);
|
/// assert_eq!(q2, Quality::MIN);
|
||||||
|
|
||||||
|
/// let q2 = q(0.0);
|
||||||
|
/// assert_eq!(q2, Quality::ZERO);
|
||||||
///
|
///
|
||||||
/// let q3 = q(0.42);
|
/// let q3 = q(0.42);
|
||||||
/// ```
|
/// ```
|
||||||
|
|
|
@ -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()
|
||||||
|
.encoding(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()
|
||||||
|
.encoding(ContentEncoding::Brotli)
|
||||||
|
.body(body)
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,10 @@ 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 `encoding` 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 get_encoding(&self) -> Option<ContentEncoding>;
|
||||||
|
@ -56,6 +59,8 @@ pub trait BodyEncoding {
|
||||||
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self;
|
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Enc(ContentEncoding);
|
||||||
|
|
||||||
impl BodyEncoding for actix_http::ResponseBuilder {
|
impl BodyEncoding for actix_http::ResponseBuilder {
|
||||||
fn get_encoding(&self) -> Option<ContentEncoding> {
|
fn get_encoding(&self) -> Option<ContentEncoding> {
|
||||||
self.extensions().get::<Enc>().map(|enc| enc.0)
|
self.extensions().get::<Enc>().map(|enc| enc.0)
|
||||||
|
@ -67,8 +72,6 @@ impl BodyEncoding for actix_http::ResponseBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Enc(ContentEncoding);
|
|
||||||
|
|
||||||
impl<B> BodyEncoding for actix_http::Response<B> {
|
impl<B> BodyEncoding for actix_http::Response<B> {
|
||||||
fn get_encoding(&self) -> Option<ContentEncoding> {
|
fn get_encoding(&self) -> Option<ContentEncoding> {
|
||||||
self.extensions().get::<Enc>().map(|enc| enc.0)
|
self.extensions().get::<Enc>().map(|enc| enc.0)
|
||||||
|
|
|
@ -68,12 +68,11 @@ common_header! {
|
||||||
common_header_test!(no_headers, vec![b""; 0], Some(AcceptEncoding(vec![])));
|
common_header_test!(no_headers, vec![b""; 0], Some(AcceptEncoding(vec![])));
|
||||||
common_header_test!(empty_header, vec![b""; 1], Some(AcceptEncoding(vec![])));
|
common_header_test!(empty_header, vec![b""; 1], Some(AcceptEncoding(vec![])));
|
||||||
|
|
||||||
// From the RFC
|
|
||||||
common_header_test!(
|
common_header_test!(
|
||||||
order_of_appearance,
|
order_of_appearance,
|
||||||
vec![b"compress, gzip"],
|
vec![b"br, gzip"],
|
||||||
Some(AcceptEncoding(vec![
|
Some(AcceptEncoding(vec![
|
||||||
QualityItem::max(Preference::Specific(Encoding::Compress)),
|
QualityItem::max(Preference::Specific(Encoding::Brotli)),
|
||||||
QualityItem::max(Preference::Specific(Encoding::Gzip)),
|
QualityItem::max(Preference::Specific(Encoding::Gzip)),
|
||||||
]))
|
]))
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,69 +1,51 @@
|
||||||
use std::{fmt, str};
|
use std::{fmt, str};
|
||||||
|
|
||||||
pub use self::Encoding::{
|
/// A value to represent an encoding used in the `Accept-Encoding` header.
|
||||||
Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, Zstd,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A value to represent an encoding used in `Transfer-Encoding` or `Accept-Encoding` header.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum Encoding {
|
pub enum Encoding {
|
||||||
/// The `chunked` encoding.
|
/// The no-op "identity" encoding.
|
||||||
Chunked,
|
|
||||||
|
|
||||||
/// The `br` encoding.
|
|
||||||
Brotli,
|
|
||||||
|
|
||||||
/// The `gzip` encoding.
|
|
||||||
Gzip,
|
|
||||||
|
|
||||||
/// The `deflate` encoding.
|
|
||||||
Deflate,
|
|
||||||
|
|
||||||
/// The `compress` encoding.
|
|
||||||
Compress,
|
|
||||||
|
|
||||||
/// The `identity` encoding. Does not affect content.
|
|
||||||
Identity,
|
Identity,
|
||||||
|
|
||||||
/// The `trailers` encoding.
|
/// Brotli compression (`br`).
|
||||||
Trailers,
|
Brotli,
|
||||||
|
|
||||||
/// The `zstd` encoding.
|
/// Gzip compression.
|
||||||
|
Gzip,
|
||||||
|
|
||||||
|
/// Deflate (LZ77) encoding.
|
||||||
|
Deflate,
|
||||||
|
|
||||||
|
/// Zstd compression.
|
||||||
Zstd,
|
Zstd,
|
||||||
|
|
||||||
/// Some other encoding that is less common, can be any String.
|
/// Some other encoding that is less common, can be any String.
|
||||||
EncodingExt(String),
|
Other(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::Identity => "identity",
|
||||||
Brotli => "br",
|
Encoding::Brotli => "br",
|
||||||
Gzip => "gzip",
|
Encoding::Gzip => "gzip",
|
||||||
Deflate => "deflate",
|
Encoding::Deflate => "deflate",
|
||||||
Compress => "compress",
|
Encoding::Zstd => "zstd",
|
||||||
Identity => "identity",
|
Encoding::Other(ref enc) => enc.as_ref(),
|
||||||
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: &str) -> Result<Self, crate::error::ParseError> {
|
||||||
"chunked" => Ok(Chunked),
|
match enc_str {
|
||||||
"br" => Ok(Brotli),
|
"identity" => Ok(Self::Identity),
|
||||||
"deflate" => Ok(Deflate),
|
"br" => Ok(Self::Brotli),
|
||||||
"gzip" => Ok(Gzip),
|
"gzip" => Ok(Self::Gzip),
|
||||||
"compress" => Ok(Compress),
|
"deflate" => Ok(Self::Deflate),
|
||||||
"identity" => Ok(Identity),
|
"zstd" => Ok(Self::Zstd),
|
||||||
"trailers" => Ok(Trailers),
|
_ => Ok(Self::Other(enc_str.to_owned())),
|
||||||
"zstd" => Ok(Zstd),
|
|
||||||
_ => Ok(EncodingExt(s.to_owned())),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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::{
|
||||||
|
body::{EitherBody, MessageBody},
|
||||||
dev::BodyEncoding,
|
dev::BodyEncoding,
|
||||||
|
http::{
|
||||||
|
header::{self, AcceptEncoding, ContentEncoding, Encoding, HeaderValue},
|
||||||
|
StatusCode,
|
||||||
|
},
|
||||||
service::{ServiceRequest, ServiceResponse},
|
service::{ServiceRequest, ServiceResponse},
|
||||||
Error, HttpResponse,
|
Error, HttpMessage, HttpResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Middleware for compressing response payloads.
|
/// Middleware for compressing response payloads.
|
||||||
|
@ -41,18 +39,22 @@ use crate::{
|
||||||
/// .default_service(web::to(|| HttpResponse::NotFound()));
|
/// .default_service(web::to(|| HttpResponse::NotFound()));
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Compress(ContentEncoding);
|
#[non_exhaustive]
|
||||||
|
pub struct Compress;
|
||||||
|
|
||||||
impl Compress {
|
impl Compress {
|
||||||
/// Create new `Compress` middleware with the specified encoding.
|
/// Create new `Compress` middleware with the specified encoding.
|
||||||
|
// TODO: remove
|
||||||
pub fn new(encoding: ContentEncoding) -> Self {
|
pub fn new(encoding: ContentEncoding) -> Self {
|
||||||
Compress(encoding)
|
// Compress(encoding)
|
||||||
|
Compress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Compress {
|
impl Default for Compress {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Compress::new(ContentEncoding::Auto)
|
// Compress::new(ContentEncoding::Auto)
|
||||||
|
Compress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,17 +72,17 @@ where
|
||||||
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,
|
// encoding: self.0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CompressMiddleware<S> {
|
pub struct CompressMiddleware<S> {
|
||||||
service: S,
|
service: S,
|
||||||
encoding: ContentEncoding,
|
// encoding: ContentEncoding,
|
||||||
}
|
}
|
||||||
|
|
||||||
static SUPPORTED_ALGORITHM_NAMES: Lazy<String> = Lazy::new(|| {
|
static SUPPORTED_ENCODINGS_STRING: Lazy<String> = Lazy::new(|| {
|
||||||
#[allow(unused_mut)] // only unused when no compress features enabled
|
#[allow(unused_mut)] // only unused when no compress features enabled
|
||||||
let mut encoding: Vec<&str> = vec![];
|
let mut encoding: Vec<&str> = vec![];
|
||||||
|
|
||||||
|
@ -96,7 +98,9 @@ static SUPPORTED_ALGORITHM_NAMES: Lazy<String> = Lazy::new(|| {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "compress-zstd")]
|
#[cfg(feature = "compress-zstd")]
|
||||||
encoding.push("zstd");
|
{
|
||||||
|
encoding.push("zstd");
|
||||||
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
!encoding.is_empty(),
|
!encoding.is_empty(),
|
||||||
|
@ -106,6 +110,33 @@ static SUPPORTED_ALGORITHM_NAMES: Lazy<String> = Lazy::new(|| {
|
||||||
encoding.join(", ")
|
encoding.join(", ")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static SUPPORTED_ENCODINGS: Lazy<Vec<Encoding>> = Lazy::new(|| {
|
||||||
|
let mut encodings = vec![Encoding::Identity];
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-brotli")]
|
||||||
|
{
|
||||||
|
encodings.push(Encoding::Brotli);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
|
{
|
||||||
|
encodings.push(Encoding::Gzip);
|
||||||
|
encodings.push(Encoding::Deflate);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-zstd")]
|
||||||
|
{
|
||||||
|
encodings.push(Encoding::Zstd);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!encodings.is_empty(),
|
||||||
|
"encodings can not be empty unless __compress feature has been explicitly enabled by itself"
|
||||||
|
);
|
||||||
|
|
||||||
|
encodings
|
||||||
|
});
|
||||||
|
|
||||||
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 +152,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 +200,7 @@ pin_project! {
|
||||||
{
|
{
|
||||||
#[pin]
|
#[pin]
|
||||||
fut: S::Future,
|
fut: S::Future,
|
||||||
encoding: ContentEncoding,
|
encoding: Encoding,
|
||||||
_phantom: PhantomData<B>,
|
_phantom: PhantomData<B>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,7 +220,14 @@ where
|
||||||
let enc = if let Some(enc) = resp.response().get_encoding() {
|
let enc = if let Some(enc) = resp.response().get_encoding() {
|
||||||
enc
|
enc
|
||||||
} else {
|
} else {
|
||||||
*this.encoding
|
match this.encoding {
|
||||||
|
Encoding::Brotli => ContentEncoding::Brotli,
|
||||||
|
Encoding::Gzip => ContentEncoding::Gzip,
|
||||||
|
Encoding::Deflate => ContentEncoding::Deflate,
|
||||||
|
Encoding::Identity => ContentEncoding::Identity,
|
||||||
|
Encoding::Zstd => ContentEncoding::Zstd,
|
||||||
|
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| {
|
||||||
|
@ -197,179 +239,3 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AcceptEncoding {
|
|
||||||
encoding: ContentEncoding,
|
|
||||||
// TODO: use Quality or QualityItem<ContentEncoding>
|
|
||||||
quality: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for AcceptEncoding {}
|
|
||||||
|
|
||||||
impl Ord for AcceptEncoding {
|
|
||||||
#[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
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
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
|
|
||||||
/// based on middleware configuration.
|
|
||||||
pub fn try_parse(
|
|
||||||
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 {
|
|
||||||
($raw:expr) => {
|
|
||||||
assert!(AcceptEncoding::try_parse($raw, ContentEncoding::Auto).is_err());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_encoding() {
|
|
||||||
// Test simple case
|
|
||||||
assert_parse_eq!("br", ContentEncoding::Br);
|
|
||||||
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]
|
|
||||||
fn test_parse_encoding_qfactor_ordering() {
|
|
||||||
assert_parse_eq!("gzip, br, zstd", ContentEncoding::Gzip);
|
|
||||||
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]
|
|
||||||
fn test_parse_encoding_qfactor_invalid() {
|
|
||||||
// Out of range
|
|
||||||
assert_parse_eq!("gzip;q=-5.0", ContentEncoding::Identity);
|
|
||||||
assert_parse_eq!("gzip;q=5.0", ContentEncoding::Identity);
|
|
||||||
|
|
||||||
// Disabled
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -0,0 +1,279 @@
|
||||||
|
use actix_http::ContentEncoding;
|
||||||
|
use actix_web::{
|
||||||
|
dev::BodyEncoding as _,
|
||||||
|
http::{header, StatusCode},
|
||||||
|
middleware::Compress,
|
||||||
|
web, App, HttpResponse,
|
||||||
|
};
|
||||||
|
use bytes::Bytes;
|
||||||
|
|
||||||
|
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
|
||||||
|
.encoding(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
|
||||||
|
.encoding(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
|
||||||
|
.encoding(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
|
||||||
|
.encoding(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));
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
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
|
||||||
|
#[ignore]
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn deny_identity_for_manual_coding() {
|
||||||
|
let srv = test_server!();
|
||||||
|
|
||||||
|
let req = srv
|
||||||
|
.post("/static")
|
||||||
|
// 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;
|
||||||
|
}
|
|
@ -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.
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -366,7 +366,7 @@ 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::new(ContentEncoding::Brotli))
|
||||||
.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))
|
||||||
|
@ -473,7 +473,7 @@ 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::new(ContentEncoding::Brotli))
|
||||||
.service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR))))
|
.service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR))))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue