Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions tower-http/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`DefaultOnEos` now explicitly parent their tracing events to the request span
rather than relying on the ambient span context. This fixes intermittent cases
where events could appear without their request span attached ([#655])
- **breaking:** `compression`: the middleware now handles the `*` wildcard and
`identity;q=0` in Accept-Encoding per RFC 9110 §12.5.3. Requests that
previously fell back to identity (e.g. `*;q=0` or `identity;q=0` with no
other acceptable encoding) now receive a 406 Not Acceptable response. Clients
that explicitly reject all encodings without listing an alternative will see
different behavior. ([#215])
- The implicit `tokio` and `async-compression` features are removed (BREAKING).
These were kept as no-op features in 0.6.x for backwards compatibility after
the switch to `dep:` syntax in [#642]. Downstream crates that activate
Expand All @@ -27,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
by the features that need them (e.g. `compression-gzip`, `fs`, `timeout`).
([#628])

[#215]: https://github.com/tower-rs/tower-http/issues/215
[#628]: https://github.com/tower-rs/tower-http/pull/628
[#642]: https://github.com/tower-rs/tower-http/pull/642

Expand Down
33 changes: 30 additions & 3 deletions tower-http/src/compression/future.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pin_project! {
pub struct ResponseFuture<F, P> {
#[pin]
pub(crate) inner: F,
pub(crate) encoding: Encoding,
pub(crate) encoding: Option<Encoding>,
pub(crate) predicate: P,
pub(crate) quality: CompressionLevel,
}
Expand All @@ -39,6 +39,33 @@ where
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let res = ready!(self.as_mut().project().inner.poll(cx)?);

let encoding = match self.encoding {
Some(enc) => enc,
None => {
// RFC 9110 §12.5.3: the server SHOULD respond with 406 Not Acceptable
// when no encoding is satisfiable. This middleware chooses to enforce it.
//
// Note: the inner service has already been called, so its response body and
// headers are passed through. Only the status code is overwritten.
let mut res = res;
*res.status_mut() = http::StatusCode::NOT_ACCEPTABLE;
if !res.headers().get_all(header::VARY).iter().any(|value| {
contains_ignore_ascii_case(
value.as_bytes(),
header::ACCEPT_ENCODING.as_str().as_bytes(),
)
}) {
res.headers_mut()
.append(header::VARY, header::ACCEPT_ENCODING.into());
}
let (parts, body) = res.into_parts();
return Poll::Ready(Ok(Response::from_parts(
parts,
CompressionBody::new(BodyInner::identity(body)),
)));
}
};

// never recompress responses that are already compressed
let should_compress = !res.headers().contains_key(header::CONTENT_ENCODING)
// never compress responses that are ranges
Expand All @@ -60,7 +87,7 @@ where
.append(header::VARY, header::ACCEPT_ENCODING.into());
}

let body = match (should_compress, self.encoding) {
let body = match (should_compress, encoding) {
// if compression is _not_ supported or the client doesn't accept it
(false, _) | (_, Encoding::Identity) => {
return Poll::Ready(Ok(Response::from_parts(
Expand Down Expand Up @@ -114,7 +141,7 @@ where

parts
.headers
.insert(header::CONTENT_ENCODING, self.encoding.into_header_value());
.insert(header::CONTENT_ENCODING, encoding.into_header_value());

let res = Response::from_parts(parts, body);
Poll::Ready(Ok(res))
Expand Down
89 changes: 89 additions & 0 deletions tower-http/src/compression/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -616,4 +616,93 @@ mod tests {
let body = res.into_body();
assert_eq!(body.size_hint().exact().unwrap(), msg.len() as u64);
}

#[tokio::test]
async fn wildcard_q_zero_returns_406() {
let svc = service_fn(handle);
let mut svc = Compression::new(svc).compress_when(Always);

let req = Request::builder()
.header("accept-encoding", "*;q=0")
.body(Body::empty())
.unwrap();
let res = svc.ready().await.unwrap().call(req).await.unwrap();

assert_eq!(res.status(), http::StatusCode::NOT_ACCEPTABLE);
assert!(res
.headers()
.get_all(http::header::VARY)
.iter()
.any(|v| v.to_str().unwrap().contains("accept-encoding")));
}

#[tokio::test]
async fn wildcard_q_zero_with_gzip_picks_gzip() {
let svc = service_fn(handle);
let mut svc = Compression::new(svc).compress_when(Always);

let req = Request::builder()
.header("accept-encoding", "*;q=0,gzip")
.body(Body::empty())
.unwrap();
let res = svc.ready().await.unwrap().call(req).await.unwrap();

assert_eq!(res.status(), http::StatusCode::OK);
assert_eq!(
res.headers()
.get("content-encoding")
.and_then(|v| v.to_str().ok()),
Some("gzip")
);
}

#[tokio::test]
async fn wildcard_alone_compresses() {
let svc = service_fn(handle);
let mut svc = Compression::new(svc).compress_when(Always);

let req = Request::builder()
.header("accept-encoding", "*")
.body(Body::empty())
.unwrap();
let res = svc.ready().await.unwrap().call(req).await.unwrap();

assert_eq!(res.status(), http::StatusCode::OK);
// Should pick the best supported encoding (not identity)
assert!(res.headers().contains_key(CONTENT_ENCODING));
}

#[tokio::test]
async fn identity_q_zero_alone_returns_406() {
let svc = service_fn(handle);
let mut svc = Compression::new(svc).compress_when(Always);

let req = Request::builder()
.header("accept-encoding", "identity;q=0")
.body(Body::empty())
.unwrap();
let res = svc.ready().await.unwrap().call(req).await.unwrap();

assert_eq!(res.status(), http::StatusCode::NOT_ACCEPTABLE);
}

#[tokio::test]
async fn identity_q_zero_with_gzip_picks_gzip() {
let svc = service_fn(handle);
let mut svc = Compression::new(svc).compress_when(Always);

let req = Request::builder()
.header("accept-encoding", "identity;q=0,gzip")
.body(Body::empty())
.unwrap();
let res = svc.ready().await.unwrap().call(req).await.unwrap();

assert_eq!(res.status(), http::StatusCode::OK);
assert_eq!(
res.headers()
.get("content-encoding")
.and_then(|v| v.to_str().ok()),
Some("gzip")
);
}
}
Loading