diff --git a/tower-http/CHANGELOG.md b/tower-http/CHANGELOG.md index d59ab922..2f31ac24 100644 --- a/tower-http/CHANGELOG.md +++ b/tower-http/CHANGELOG.md @@ -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 @@ -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 diff --git a/tower-http/src/compression/future.rs b/tower-http/src/compression/future.rs index 3e899a73..e19ecd6f 100644 --- a/tower-http/src/compression/future.rs +++ b/tower-http/src/compression/future.rs @@ -22,7 +22,7 @@ pin_project! { pub struct ResponseFuture { #[pin] pub(crate) inner: F, - pub(crate) encoding: Encoding, + pub(crate) encoding: Option, pub(crate) predicate: P, pub(crate) quality: CompressionLevel, } @@ -39,6 +39,33 @@ where fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 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 @@ -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( @@ -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)) diff --git a/tower-http/src/compression/mod.rs b/tower-http/src/compression/mod.rs index c32fc579..044fca51 100644 --- a/tower-http/src/compression/mod.rs +++ b/tower-http/src/compression/mod.rs @@ -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") + ); + } } diff --git a/tower-http/src/content_encoding.rs b/tower-http/src/content_encoding.rs index 91c21d45..18ccacb1 100644 --- a/tower-http/src/content_encoding.rs +++ b/tower-http/src/content_encoding.rs @@ -96,12 +96,14 @@ impl Encoding { feature = "compression-deflate", ))] // based on https://github.com/http-rs/accept-encoding + // + // Returns `Some(encoding)` for the best acceptable encoding, or `None` if the client's + // preferences cannot be satisfied (406 Not Acceptable per RFC 9110 §12.5.3). pub(crate) fn from_headers( headers: &http::HeaderMap, supported_encoding: impl SupportedEncodings, - ) -> Self { - Encoding::preferred_encoding(encodings(headers, supported_encoding)) - .unwrap_or(Encoding::Identity) + ) -> Option { + preferred_encoding_with_wildcard(headers, supported_encoding) } #[cfg(any( @@ -240,6 +242,135 @@ pub(crate) fn encodings<'a>( }) } +/// Extracts the q-value for the `*` wildcard from Accept-Encoding headers. +/// Returns `None` if no wildcard is present. +#[cfg(any( + feature = "compression-gzip", + feature = "compression-br", + feature = "compression-zstd", + feature = "compression-deflate", +))] +fn wildcard_qvalue(headers: &http::HeaderMap) -> Option { + headers + .get_all(http::header::ACCEPT_ENCODING) + .iter() + .filter_map(|hval| hval.to_str().ok()) + .flat_map(|s| s.split(',')) + .find_map(|v| { + let mut v = v.splitn(2, ';'); + let coding = v.next().unwrap().trim(); + if coding != "*" { + return None; + } + let qval = if let Some(qval) = v.next() { + QValue::parse(qval.trim())? + } else { + QValue::one() + }; + Some(qval) + }) +} + +/// Selects the preferred encoding considering the `*` wildcard per RFC 9110 §12.5.3. +/// +/// The wildcard applies its q-value to any encoding not explicitly listed. If all acceptable +/// encodings (including identity) are excluded, returns `None` to signal 406 Not Acceptable. +#[cfg(any( + feature = "compression-gzip", + feature = "compression-br", + feature = "compression-zstd", + feature = "compression-deflate", +))] +fn preferred_encoding_with_wildcard( + headers: &http::HeaderMap, + supported_encoding: impl SupportedEncodings, +) -> Option { + let explicit: Vec<(Encoding, QValue)> = encodings(headers, supported_encoding).collect(); + let wildcard_q = wildcard_qvalue(headers); + + // If there is no wildcard, use only the explicitly listed encodings. + // Per RFC 9110 §12.5.3, if identity is excluded (q=0) and no other encoding is + // acceptable, the server SHOULD respond with 406. + let wildcard_q = match wildcard_q { + Some(q) => q, + None => { + let identity_rejected = explicit + .iter() + .any(|(enc, q)| *enc == Encoding::Identity && q.0 == 0); + return match Encoding::preferred_encoding(explicit.into_iter()) { + Some(enc) => Some(enc), + None => { + if identity_rejected { + None + } else { + Some(Encoding::Identity) + } + } + }; + } + }; + + // Build the effective set of (encoding, qvalue) for all supported encodings. + // For each supported encoding, use its explicit q-value if listed, otherwise the wildcard + // q-value. + let all_supported = all_supported_encodings(supported_encoding); + + let effective = all_supported.iter().filter_map(|e| *e).map(|enc| { + let q = explicit + .iter() + .find(|(e, _)| *e == enc) + .map(|(_, q)| *q) + .unwrap_or(wildcard_q); + (enc, q) + }); + + Encoding::preferred_encoding(effective) +} + +/// Returns all encodings the server supports (including Identity) in a fixed-capacity array. +#[cfg(any( + feature = "compression-gzip", + feature = "compression-br", + feature = "compression-zstd", + feature = "compression-deflate", +))] +fn all_supported_encodings(supported_encoding: impl SupportedEncodings) -> [Option; 5] { + let mut out: [Option; 5] = [None; 5]; + let mut n = 0; + + macro_rules! push { + ($enc:expr) => { + out[n] = Some($enc); + n += 1; + }; + } + + push!(Encoding::Identity); + + #[cfg(any(feature = "fs", feature = "compression-gzip"))] + if supported_encoding.gzip() { + push!(Encoding::Gzip); + } + + #[cfg(any(feature = "fs", feature = "compression-deflate"))] + if supported_encoding.deflate() { + push!(Encoding::Deflate); + } + + #[cfg(any(feature = "fs", feature = "compression-br"))] + if supported_encoding.br() { + push!(Encoding::Brotli); + } + + #[cfg(any(feature = "fs", feature = "compression-zstd"))] + if supported_encoding.zstd() { + push!(Encoding::Zstd); + } + + let _ = n; + out +} + #[cfg(all( test, feature = "compression-gzip", @@ -274,7 +405,7 @@ mod tests { #[test] fn no_accept_encoding_header() { let encoding = Encoding::from_headers(&http::HeaderMap::new(), SupportedEncodingsAll); - assert_eq!(Encoding::Identity, encoding); + assert_eq!(Some(Encoding::Identity), encoding); } #[test] @@ -285,7 +416,7 @@ mod tests { http::HeaderValue::from_static("gzip"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Gzip, encoding); + assert_eq!(Some(Encoding::Gzip), encoding); } #[test] @@ -296,7 +427,7 @@ mod tests { http::HeaderValue::from_static("gzip,br"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Brotli, encoding); + assert_eq!(Some(Encoding::Brotli), encoding); } #[test] @@ -307,7 +438,7 @@ mod tests { http::HeaderValue::from_static("gzip,x-gzip"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Gzip, encoding); + assert_eq!(Some(Encoding::Gzip), encoding); } #[test] @@ -318,7 +449,7 @@ mod tests { http::HeaderValue::from_static("deflate,x-gzip"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Gzip, encoding); + assert_eq!(Some(Encoding::Gzip), encoding); } #[test] @@ -329,7 +460,7 @@ mod tests { http::HeaderValue::from_static("gzip,deflate,br"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Brotli, encoding); + assert_eq!(Some(Encoding::Brotli), encoding); } #[test] @@ -340,7 +471,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=0.5,br"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Brotli, encoding); + assert_eq!(Some(Encoding::Brotli), encoding); } #[test] @@ -351,7 +482,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=0.5,deflate,br"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Brotli, encoding); + assert_eq!(Some(Encoding::Brotli), encoding); } #[test] @@ -366,7 +497,7 @@ mod tests { http::HeaderValue::from_static("br"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Brotli, encoding); + assert_eq!(Some(Encoding::Brotli), encoding); } #[test] @@ -381,7 +512,7 @@ mod tests { http::HeaderValue::from_static("br"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Brotli, encoding); + assert_eq!(Some(Encoding::Brotli), encoding); } #[test] @@ -400,7 +531,7 @@ mod tests { http::HeaderValue::from_static("br"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Brotli, encoding); + assert_eq!(Some(Encoding::Brotli), encoding); } #[test] @@ -411,7 +542,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=0.5,br;q=0.8"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Brotli, encoding); + assert_eq!(Some(Encoding::Brotli), encoding); let mut headers = http::HeaderMap::new(); headers.append( @@ -419,7 +550,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=0.8,br;q=0.5"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Gzip, encoding); + assert_eq!(Some(Encoding::Gzip), encoding); let mut headers = http::HeaderMap::new(); headers.append( @@ -427,7 +558,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=0.995,br;q=0.999"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Brotli, encoding); + assert_eq!(Some(Encoding::Brotli), encoding); } #[test] @@ -438,7 +569,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=0.5,deflate;q=0.6,br;q=0.8"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Brotli, encoding); + assert_eq!(Some(Encoding::Brotli), encoding); let mut headers = http::HeaderMap::new(); headers.append( @@ -446,7 +577,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=0.8,deflate;q=0.6,br;q=0.5"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Gzip, encoding); + assert_eq!(Some(Encoding::Gzip), encoding); let mut headers = http::HeaderMap::new(); headers.append( @@ -454,7 +585,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=0.6,deflate;q=0.8,br;q=0.5"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Deflate, encoding); + assert_eq!(Some(Encoding::Deflate), encoding); let mut headers = http::HeaderMap::new(); headers.append( @@ -462,7 +593,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=0.995,deflate;q=0.997,br;q=0.999"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Brotli, encoding); + assert_eq!(Some(Encoding::Brotli), encoding); } #[test] @@ -473,7 +604,7 @@ mod tests { http::HeaderValue::from_static("invalid,gzip"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Gzip, encoding); + assert_eq!(Some(Encoding::Gzip), encoding); } #[test] @@ -484,7 +615,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=0"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Identity, encoding); + assert_eq!(Some(Encoding::Identity), encoding); let mut headers = http::HeaderMap::new(); headers.append( @@ -492,7 +623,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=0."), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Identity, encoding); + assert_eq!(Some(Encoding::Identity), encoding); let mut headers = http::HeaderMap::new(); headers.append( @@ -500,7 +631,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=0,br;q=0.5"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Brotli, encoding); + assert_eq!(Some(Encoding::Brotli), encoding); } #[test] @@ -511,7 +642,7 @@ mod tests { http::HeaderValue::from_static("gZiP"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Gzip, encoding); + assert_eq!(Some(Encoding::Gzip), encoding); let mut headers = http::HeaderMap::new(); headers.append( @@ -519,7 +650,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=0.5,br;Q=0.8"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Brotli, encoding); + assert_eq!(Some(Encoding::Brotli), encoding); } #[test] @@ -530,7 +661,7 @@ mod tests { http::HeaderValue::from_static(" gzip\t; q=0.5 ,\tbr ;\tq=0.8\t"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Brotli, encoding); + assert_eq!(Some(Encoding::Brotli), encoding); } #[test] @@ -541,7 +672,7 @@ mod tests { http::HeaderValue::from_static("gzip;q =0.5"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Identity, encoding); + assert_eq!(Some(Encoding::Identity), encoding); let mut headers = http::HeaderMap::new(); headers.append( @@ -549,7 +680,7 @@ mod tests { http::HeaderValue::from_static("gzip;q= 0.5"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Identity, encoding); + assert_eq!(Some(Encoding::Identity), encoding); } #[test] @@ -560,7 +691,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=-0.1"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Identity, encoding); + assert_eq!(Some(Encoding::Identity), encoding); let mut headers = http::HeaderMap::new(); headers.append( @@ -568,7 +699,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=00.5"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Identity, encoding); + assert_eq!(Some(Encoding::Identity), encoding); let mut headers = http::HeaderMap::new(); headers.append( @@ -576,7 +707,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=0.5000"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Identity, encoding); + assert_eq!(Some(Encoding::Identity), encoding); let mut headers = http::HeaderMap::new(); headers.append( @@ -584,7 +715,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=.5"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Identity, encoding); + assert_eq!(Some(Encoding::Identity), encoding); let mut headers = http::HeaderMap::new(); headers.append( @@ -592,7 +723,7 @@ mod tests { http::HeaderValue::from_static("gzip;q=1.01"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Identity, encoding); + assert_eq!(Some(Encoding::Identity), encoding); let mut headers = http::HeaderMap::new(); headers.append( @@ -600,6 +731,146 @@ mod tests { http::HeaderValue::from_static("gzip;q=1.001"), ); let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); - assert_eq!(Encoding::Identity, encoding); + assert_eq!(Some(Encoding::Identity), encoding); + } + + #[test] + fn wildcard_alone_picks_best_supported() { + let mut headers = http::HeaderMap::new(); + headers.append( + http::header::ACCEPT_ENCODING, + http::HeaderValue::from_static("*"), + ); + let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); + // * with q=1 means all encodings are acceptable; picks the highest-priority supported + assert_eq!(Some(Encoding::Zstd), encoding); + } + + #[test] + fn wildcard_q_zero_with_nothing_else_returns_not_satisfiable() { + let mut headers = http::HeaderMap::new(); + headers.append( + http::header::ACCEPT_ENCODING, + http::HeaderValue::from_static("*;q=0"), + ); + let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); + // *;q=0 rejects everything, including identity + assert_eq!(None, encoding); + } + + #[test] + fn wildcard_q_zero_with_gzip_picks_gzip() { + let mut headers = http::HeaderMap::new(); + headers.append( + http::header::ACCEPT_ENCODING, + http::HeaderValue::from_static("*;q=0,gzip"), + ); + let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); + assert_eq!(Some(Encoding::Gzip), encoding); + } + + #[test] + fn identity_q_zero_alone_returns_not_satisfiable() { + let mut headers = http::HeaderMap::new(); + headers.append( + http::header::ACCEPT_ENCODING, + http::HeaderValue::from_static("identity;q=0"), + ); + let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); + // identity;q=0 with no other encoding explicitly listed: the server cannot + // determine what the client accepts, so 406 per RFC 9110 §12.5.3 + assert_eq!(None, encoding); + } + + #[test] + fn identity_q_zero_with_gzip_picks_gzip() { + let mut headers = http::HeaderMap::new(); + headers.append( + http::header::ACCEPT_ENCODING, + http::HeaderValue::from_static("identity;q=0,gzip"), + ); + let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); + assert_eq!(Some(Encoding::Gzip), encoding); + } + + #[test] + fn wildcard_q_zero_identity_q_zero_no_compression_returns_not_satisfiable() { + // *;q=0,identity;q=0 with no explicit compression listed + let mut headers = http::HeaderMap::new(); + headers.append( + http::header::ACCEPT_ENCODING, + http::HeaderValue::from_static("*;q=0,identity;q=0"), + ); + let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); + // Both wildcard and identity are q=0, and no explicit encoding is listed with q>0 + assert_eq!(None, encoding); + } + + #[test] + fn wildcard_with_low_qvalue() { + let mut headers = http::HeaderMap::new(); + headers.append( + http::header::ACCEPT_ENCODING, + http::HeaderValue::from_static("*;q=0.5,gzip;q=1"), + ); + let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); + // gzip is explicitly q=1, everything else gets q=0.5 from wildcard + assert_eq!(Some(Encoding::Gzip), encoding); + } + + #[test] + fn wildcard_q_zero_with_identity_picks_identity() { + let mut headers = http::HeaderMap::new(); + headers.append( + http::header::ACCEPT_ENCODING, + http::HeaderValue::from_static("*;q=0,identity"), + ); + let encoding = Encoding::from_headers(&headers, SupportedEncodingsAll); + // *;q=0 rejects all, but identity is explicitly listed with q=1 + assert_eq!(Some(Encoding::Identity), encoding); + } + + #[derive(Copy, Clone)] + struct SupportedGzipOnly; + + impl SupportedEncodings for SupportedGzipOnly { + fn gzip(&self) -> bool { + true + } + fn deflate(&self) -> bool { + false + } + fn br(&self) -> bool { + false + } + fn zstd(&self) -> bool { + false + } + } + + #[test] + fn wildcard_with_partial_server_support_picks_best_available() { + let mut headers = http::HeaderMap::new(); + headers.append( + http::header::ACCEPT_ENCODING, + http::HeaderValue::from_static("*"), + ); + let encoding = Encoding::from_headers(&headers, SupportedGzipOnly); + // Server only supports gzip, so * should pick gzip (not zstd/br) + assert_eq!(Some(Encoding::Gzip), encoding); + } + + #[test] + fn wildcard_q_zero_with_unsupported_encoding_returns_not_satisfiable() { + let mut headers = http::HeaderMap::new(); + headers.append( + http::header::ACCEPT_ENCODING, + http::HeaderValue::from_static("*;q=0,br"), + ); + let encoding = Encoding::from_headers(&headers, SupportedGzipOnly); + // Client wants br, but server only supports gzip. br is not in the + // supported set so it's ignored by encodings(). Wildcard rejects + // everything else. Result: 406. + assert_eq!(None, encoding); } } diff --git a/tower-http/src/services/fs/serve_dir/headers.rs b/tower-http/src/services/fs/serve_dir/headers.rs index e9e80907..e3a87a2c 100644 --- a/tower-http/src/services/fs/serve_dir/headers.rs +++ b/tower-http/src/services/fs/serve_dir/headers.rs @@ -18,7 +18,7 @@ impl IfModifiedSince { self.0 < last_modified.0 } - /// convert a header value into a IfModifiedSince, invalid values are silentely ignored + /// Convert a header value into a IfModifiedSince. Invalid values are silently ignored pub(super) fn from_header_value(value: &HeaderValue) -> Option { std::str::from_utf8(value.as_bytes()) .ok() @@ -35,7 +35,7 @@ impl IfUnmodifiedSince { self.0 >= last_modified.0 } - /// Convert a header value into a IfModifiedSince, invalid values are silentely ignored + /// Convert a header value into a IfUnmodifiedSince. Invalid values are silently ignored pub(super) fn from_header_value(value: &HeaderValue) -> Option { std::str::from_utf8(value.as_bytes()) .ok() diff --git a/tower-http/src/services/fs/serve_dir/mod.rs b/tower-http/src/services/fs/serve_dir/mod.rs index b243202c..bab51014 100644 --- a/tower-http/src/services/fs/serve_dir/mod.rs +++ b/tower-http/src/services/fs/serve_dir/mod.rs @@ -54,7 +54,7 @@ pub struct ServeDir { base: PathBuf, buf_chunk_size: usize, precompressed_variants: Option, - // This is used to specialise implementation for + // This is used to specialize implementation for // single files variant: ServeVariant, fallback: Option, @@ -296,7 +296,7 @@ impl ServeDir { /// let mut service = ServeDir::new("assets"); /// /// // You only need to worry about backpressure, and thus call `ServiceExt::ready`, if - /// // your adding a fallback to `ServeDir` that cares about backpressure. + /// // you are adding a fallback to `ServeDir` that cares about backpressure. /// // /// // Its shown here for demonstration but you can do `service.try_call(request)` /// // otherwise diff --git a/tower-http/src/services/fs/serve_dir/tests.rs b/tower-http/src/services/fs/serve_dir/tests.rs index 3dd9a086..0b9a6c78 100644 --- a/tower-http/src/services/fs/serve_dir/tests.rs +++ b/tower-http/src/services/fs/serve_dir/tests.rs @@ -75,7 +75,7 @@ async fn head_request() { } #[tokio::test] -async fn precompresed_head_request() { +async fn precompressed_head_request() { let svc = ServeDir::new(TEST_FILES_DIR).precompressed_gzip(); let req = Request::builder() diff --git a/tower-http/src/services/fs/serve_file.rs b/tower-http/src/services/fs/serve_file.rs index d3b5e2f0..efd216c5 100644 --- a/tower-http/src/services/fs/serve_file.rs +++ b/tower-http/src/services/fs/serve_file.rs @@ -208,7 +208,7 @@ mod tests { } #[tokio::test] - async fn precompresed_head_request() { + async fn precompressed_head_request() { let svc = ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt")).precompressed_gzip();