#![cfg(feature = "tokio")]
use std::convert::Infallible;
#[cfg(any(
feature = "gzip",
feature = "deflate",
feature = "brotli",
feature = "zstd"
))]
use std::sync::atomic::{AtomicU32, Ordering};
#[cfg(any(
feature = "gzip",
feature = "deflate",
feature = "brotli",
feature = "zstd"
))]
use std::sync::{Arc, Mutex};
#[cfg(any(
feature = "gzip",
feature = "deflate",
feature = "brotli",
feature = "zstd"
))]
use std::time::Duration;
use bytes::Bytes;
use http_body_util::Full;
#[cfg(any(
feature = "gzip",
feature = "deflate",
feature = "brotli",
feature = "zstd"
))]
use hyper::Request;
use hyper::Response;
use aioduct::HttpEngineSend;
use aioduct::runtime::TokioRuntime;
use aioduct::runtime::tokio_rt::TcpConnector;
use aioduct_test_server::h1::h1_server_with;
#[cfg(any(
feature = "gzip",
feature = "deflate",
feature = "brotli",
feature = "zstd"
))]
use aioduct_test_server::raw::raw_streaming_server;
#[cfg(feature = "gzip")]
#[tokio::test]
async fn test_gzip_decompression() {
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
let handler = |_req: Request<hyper::body::Incoming>| async {
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(b"hello compressed world").unwrap();
let compressed = encoder.finish().unwrap();
let resp = Response::builder()
.header("content-encoding", "gzip")
.body(Full::new(Bytes::from(compressed)))
.unwrap();
Ok::<_, Infallible>(resp)
};
let (addr, _counter) = h1_server_with(handler).await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert!(!resp.headers().contains_key("content-encoding"));
let text = resp.text().await.unwrap();
assert_eq!(text, "hello compressed world");
}
#[cfg(feature = "gzip")]
#[tokio::test]
async fn test_gzip_accept_encoding_header() {
let handler = |req: Request<hyper::body::Incoming>| async move {
let accept = req
.headers()
.get("accept-encoding")
.map(|v| v.to_str().unwrap().to_string())
.unwrap_or_default();
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from(accept))))
};
let (addr, _counter) = h1_server_with(handler).await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let text = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap()
.text()
.await
.unwrap();
assert!(text.contains("gzip"));
}
#[cfg(feature = "gzip")]
#[tokio::test]
async fn test_no_decompression_passthrough() {
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
let handler = |_req: Request<hyper::body::Incoming>| async {
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(b"raw gzip data").unwrap();
let compressed = encoder.finish().unwrap();
let resp = Response::builder()
.header("content-encoding", "gzip")
.body(Full::new(Bytes::from(compressed)))
.unwrap();
Ok::<_, Infallible>(resp)
};
let (addr, _counter) = h1_server_with(handler).await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.no_decompression()
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert!(resp.headers().contains_key("content-encoding"));
let bytes = resp.bytes().await.unwrap();
assert_ne!(bytes.as_ref(), b"raw gzip data");
}
#[cfg(feature = "deflate")]
#[tokio::test]
async fn test_deflate_decompression() {
use flate2::Compression;
use flate2::write::ZlibEncoder;
use std::io::Write;
let handler = |_req: Request<hyper::body::Incoming>| async {
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(b"deflate test payload").unwrap();
let compressed = encoder.finish().unwrap();
let resp = Response::builder()
.header("content-encoding", "deflate")
.body(Full::new(Bytes::from(compressed)))
.unwrap();
Ok::<_, Infallible>(resp)
};
let (addr, _counter) = h1_server_with(handler).await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let text = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap()
.text()
.await
.unwrap();
assert_eq!(text, "deflate test payload");
}
#[tokio::test]
async fn test_get_no_content_headers() {
let (addr, _counter) = h1_server_with(|req| async move {
assert_eq!(req.method(), "GET");
assert!(
req.headers().get("content-length").is_none(),
"GET should not have content-length"
);
assert!(
req.headers().get("content-type").is_none(),
"GET should not have content-type"
);
assert!(
req.headers().get("transfer-encoding").is_none(),
"GET should not have transfer-encoding"
);
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from("ok"))))
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
}
#[cfg(feature = "gzip")]
#[tokio::test]
async fn test_gzip_empty_body_head_request() {
let (addr, _counter) = h1_server_with(|req| async move {
assert_eq!(req.method(), "HEAD");
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "gzip")
.body(Full::new(Bytes::new()))
.unwrap(),
)
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.head(&format!("http://{addr}/gzip"))
.unwrap()
.send()
.await
.unwrap();
let body = resp.text().await.unwrap();
assert_eq!(body, "");
}
#[cfg(feature = "gzip")]
#[tokio::test]
async fn test_custom_accept_encoding_preserved() {
let (addr, _counter) = h1_server_with(|req| async move {
let accept_encoding = req
.headers()
.get("accept-encoding")
.map(|v| v.to_str().unwrap().to_owned())
.unwrap_or_default();
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from(accept_encoding))))
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.header(
http::header::ACCEPT_ENCODING,
http::header::HeaderValue::from_static("identity"),
)
.send()
.await
.unwrap();
let body = resp.text().await.unwrap();
assert_eq!(body, "identity");
}
#[cfg(feature = "gzip")]
mod gzip_tests {
use super::*;
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
fn gzip_compress(input: &[u8]) -> Vec<u8> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(input).unwrap();
encoder.finish().unwrap()
}
#[tokio::test]
async fn gzip_large_body() {
let content: String = (0..10_000).map(|i| format!("test {i}")).collect();
let compressed = gzip_compress(content.as_bytes());
let (addr, _counter) = h1_server_with(move |_req| {
let compressed = compressed.clone();
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "gzip")
.header("content-length", compressed.len().to_string())
.body(Full::new(Bytes::from(compressed)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
let text = resp.text().await.unwrap();
assert_eq!(text, content);
}
#[tokio::test]
async fn gzip_empty_body_head_request() {
let (addr, _counter) = h1_server_with(|req| async move {
assert_eq!(req.method(), "HEAD");
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "gzip")
.body(Full::new(Bytes::new()))
.unwrap(),
)
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.head(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
let body = resp.text().await.unwrap();
assert_eq!(body, "");
}
#[tokio::test]
async fn gzip_accept_encoding_sent_automatically() {
let (addr, _counter) = h1_server_with(|req| async move {
let ae = req
.headers()
.get("accept-encoding")
.map(|v| v.to_str().unwrap().to_owned())
.unwrap_or_default();
assert!(
ae.contains("gzip"),
"accept-encoding should contain gzip, got: {ae}"
);
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from("ok"))))
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
}
#[tokio::test]
async fn gzip_accept_encoding_not_changed_if_set() {
let (addr, _counter) = h1_server_with(|req| async move {
let ae = req
.headers()
.get("accept-encoding")
.map(|v| v.to_str().unwrap().to_owned())
.unwrap_or_default();
assert_eq!(ae, "identity");
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from("ok"))))
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.header(
http::header::ACCEPT_ENCODING,
http::HeaderValue::from_static("identity"),
)
.send()
.await
.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
}
#[tokio::test]
async fn gzip_connection_reuse_after_decompression() {
let compressed = gzip_compress(b"hello compressed");
let request_count = Arc::new(AtomicU32::new(0));
let count_clone = request_count.clone();
let (addr, _counter) = h1_server_with(move |_req| {
let compressed = compressed.clone();
let count = count_clone.clone();
async move {
count.fetch_add(1, Ordering::SeqCst);
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "gzip")
.body(Full::new(Bytes::from(compressed)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
for _ in 0..3 {
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
let text = resp.text().await.unwrap();
assert_eq!(text, "hello compressed");
}
assert_eq!(request_count.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn no_decompression_passthrough() {
let compressed = gzip_compress(b"raw gzip data");
let (addr, _counter) = h1_server_with(move |_req| {
let compressed = compressed.clone();
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "gzip")
.body(Full::new(Bytes::from(compressed)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.no_decompression()
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert!(resp.headers().contains_key("content-encoding"));
let bytes = resp.bytes().await.unwrap();
assert_ne!(&bytes[..], b"raw gzip data");
}
}
#[cfg(feature = "deflate")]
mod deflate_tests {
use super::*;
use flate2::Compression;
use flate2::write::ZlibEncoder;
use std::io::Write;
#[tokio::test]
async fn deflate_decompression() {
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(b"deflate test payload").unwrap();
let compressed = encoder.finish().unwrap();
let (addr, _counter) = h1_server_with(move |_req| {
let compressed = compressed.clone();
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "deflate")
.body(Full::new(Bytes::from(compressed)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let text = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap()
.text()
.await
.unwrap();
assert_eq!(text, "deflate test payload");
}
#[tokio::test]
async fn deflate_accept_encoding_sent() {
let (addr, _counter) = h1_server_with(|req| async move {
let ae = req
.headers()
.get("accept-encoding")
.map(|v| v.to_str().unwrap().to_owned())
.unwrap_or_default();
assert!(
ae.contains("deflate"),
"accept-encoding should contain deflate, got: {ae}"
);
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from("ok"))))
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
}
}
#[cfg(feature = "brotli")]
mod brotli_tests {
use super::*;
use std::io::Write;
fn brotli_compress(input: &[u8]) -> Vec<u8> {
let mut compressed = Vec::new();
let mut encoder = brotli::CompressorWriter::new(&mut compressed, 4096, 6, 22);
encoder.write_all(input).unwrap();
drop(encoder);
compressed
}
#[tokio::test]
async fn brotli_decompression() {
let compressed = brotli_compress(b"brotli test payload");
let (addr, _counter) = h1_server_with(move |_req| {
let compressed = compressed.clone();
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "br")
.body(Full::new(Bytes::from(compressed)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let text = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap()
.text()
.await
.unwrap();
assert_eq!(text, "brotli test payload");
}
#[tokio::test]
async fn brotli_large_body() {
let content: String = (0..5_000).map(|i| format!("brotli {i} ")).collect();
let compressed = brotli_compress(content.as_bytes());
let (addr, _counter) = h1_server_with(move |_req| {
let compressed = compressed.clone();
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "br")
.body(Full::new(Bytes::from(compressed)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let text = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap()
.text()
.await
.unwrap();
assert_eq!(text, content);
}
#[tokio::test]
async fn brotli_accept_encoding_sent() {
let (addr, _counter) = h1_server_with(|req| async move {
let ae = req
.headers()
.get("accept-encoding")
.map(|v| v.to_str().unwrap().to_owned())
.unwrap_or_default();
assert!(
ae.contains("br"),
"accept-encoding should contain br, got: {ae}"
);
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from("ok"))))
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
}
}
#[cfg(feature = "zstd")]
mod zstd_tests {
use super::*;
#[tokio::test]
async fn zstd_decompression() {
let compressed = zstd::encode_all(b"zstd test payload" as &[u8], 3).unwrap();
let (addr, _counter) = h1_server_with(move |_req| {
let compressed = compressed.clone();
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "zstd")
.body(Full::new(Bytes::from(compressed)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let text = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap()
.text()
.await
.unwrap();
assert_eq!(text, "zstd test payload");
}
#[tokio::test]
async fn zstd_large_body() {
let content: String = (0..5_000).map(|i| format!("zstd {i} ")).collect();
let compressed = zstd::encode_all(content.as_bytes(), 3).unwrap();
let (addr, _counter) = h1_server_with(move |_req| {
let compressed = compressed.clone();
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "zstd")
.body(Full::new(Bytes::from(compressed)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let text = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap()
.text()
.await
.unwrap();
assert_eq!(text, content);
}
#[tokio::test]
async fn zstd_accept_encoding_sent() {
let (addr, _counter) = h1_server_with(|req| async move {
let ae = req
.headers()
.get("accept-encoding")
.map(|v| v.to_str().unwrap().to_owned())
.unwrap_or_default();
assert!(
ae.contains("zstd"),
"accept-encoding should contain zstd, got: {ae}"
);
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from("ok"))))
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
}
}
#[cfg(feature = "gzip")]
mod fragmented_gzip {
use super::*;
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
use tokio::io::AsyncWriteExt;
#[tokio::test]
async fn gzip_chunked_fragmented_response() {
let content = "hello fragmented gzip world";
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(content.as_bytes()).unwrap();
let compressed = encoder.finish().unwrap();
let mid = compressed.len() / 2;
let part1: Vec<u8> = compressed[..mid].to_vec();
let part2: Vec<u8> = compressed[mid..].to_vec();
let addr = raw_streaming_server(move |_req, mut stream| {
let p1 = part1.clone();
let p2 = part2.clone();
async move {
let headers = "HTTP/1.1 200 OK\r\nContent-Encoding: gzip\r\nTransfer-Encoding: chunked\r\n\r\n";
stream.write_all(headers.as_bytes()).await.unwrap();
stream.flush().await.unwrap();
let chunk1 = format!("{:x}\r\n", p1.len());
stream.write_all(chunk1.as_bytes()).await.unwrap();
stream.write_all(&p1).await.unwrap();
stream.write_all(b"\r\n").await.unwrap();
stream.flush().await.unwrap();
tokio::time::sleep(Duration::from_millis(50)).await;
let chunk2 = format!("{:x}\r\n", p2.len());
stream.write_all(chunk2.as_bytes()).await.unwrap();
stream.write_all(&p2).await.unwrap();
stream.write_all(b"\r\n").await.unwrap();
stream.write_all(b"0\r\n\r\n").await.unwrap();
stream.flush().await.unwrap();
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.text().await.unwrap(), content);
}
}
#[cfg(feature = "deflate")]
mod fragmented_deflate {
use super::*;
use flate2::Compression;
use flate2::write::ZlibEncoder;
use std::io::Write;
use tokio::io::AsyncWriteExt;
#[tokio::test]
async fn deflate_chunked_fragmented_response() {
let content = "hello fragmented deflate world";
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(content.as_bytes()).unwrap();
let compressed = encoder.finish().unwrap();
let third = compressed.len() / 3;
let parts: Vec<Vec<u8>> = vec![
compressed[..third].to_vec(),
compressed[third..third * 2].to_vec(),
compressed[third * 2..].to_vec(),
];
let addr = raw_streaming_server(move |_req, mut stream| {
let parts = parts.clone();
async move {
let headers = "HTTP/1.1 200 OK\r\nContent-Encoding: deflate\r\nTransfer-Encoding: chunked\r\n\r\n";
stream.write_all(headers.as_bytes()).await.unwrap();
stream.flush().await.unwrap();
for part in &parts {
let chunk_hdr = format!("{:x}\r\n", part.len());
stream.write_all(chunk_hdr.as_bytes()).await.unwrap();
stream.write_all(part).await.unwrap();
stream.write_all(b"\r\n").await.unwrap();
stream.flush().await.unwrap();
tokio::time::sleep(Duration::from_millis(30)).await;
}
stream.write_all(b"0\r\n\r\n").await.unwrap();
stream.flush().await.unwrap();
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.text().await.unwrap(), content);
}
}
#[cfg(feature = "brotli")]
mod fragmented_brotli {
use super::*;
use tokio::io::AsyncWriteExt;
#[tokio::test]
async fn brotli_chunked_fragmented_response() {
let content = "hello fragmented brotli world";
let mut compressed = Vec::new();
{
let mut writer = brotli::CompressorWriter::new(&mut compressed, 4096, 6, 22);
std::io::Write::write_all(&mut writer, content.as_bytes()).unwrap();
drop(writer);
}
let bytes: Vec<u8> = compressed;
let addr = raw_streaming_server(move |_req, mut stream| {
let bytes = bytes.clone();
async move {
let headers =
"HTTP/1.1 200 OK\r\nContent-Encoding: br\r\nTransfer-Encoding: chunked\r\n\r\n";
stream.write_all(headers.as_bytes()).await.unwrap();
stream.flush().await.unwrap();
for byte in &bytes[..5.min(bytes.len())] {
stream.write_all(b"1\r\n").await.unwrap();
stream.write_all(&[*byte]).await.unwrap();
stream.write_all(b"\r\n").await.unwrap();
stream.flush().await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
}
if bytes.len() > 5 {
let rest = &bytes[5..];
let chunk_hdr = format!("{:x}\r\n", rest.len());
stream.write_all(chunk_hdr.as_bytes()).await.unwrap();
stream.write_all(rest).await.unwrap();
stream.write_all(b"\r\n").await.unwrap();
stream.flush().await.unwrap();
}
stream.write_all(b"0\r\n\r\n").await.unwrap();
stream.flush().await.unwrap();
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.text().await.unwrap(), content);
}
}
#[cfg(feature = "gzip")]
#[tokio::test]
async fn stacked_content_encoding_not_handled() {
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
let content = "hello stacked encoding";
let mut gzip_encoder = GzEncoder::new(Vec::new(), Compression::fast());
gzip_encoder.write_all(content.as_bytes()).unwrap();
let compressed = gzip_encoder.finish().unwrap();
let addr = raw_streaming_server(move |_request_bytes, mut stream| {
let compressed = compressed.clone();
async move {
use tokio::io::AsyncWriteExt;
let response = format!(
"HTTP/1.1 200 OK\r\n\
Content-Encoding: gzip, identity\r\n\
Content-Length: {}\r\n\
\r\n",
compressed.len()
);
stream.write_all(response.as_bytes()).await.unwrap();
stream.write_all(&compressed).await.unwrap();
stream.flush().await.unwrap();
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
let body = resp.text().await.unwrap();
assert_eq!(
body, content,
"BUG: decompress.rs:328-341 matches Content-Encoding as exact bytes. \
Stacked encodings like 'gzip, identity' are not recognized and the body \
is returned still compressed."
);
}
#[cfg(feature = "gzip")]
mod case_insensitive_encoding {
use super::*;
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
use tokio::io::AsyncWriteExt;
fn gzip_compress(input: &[u8]) -> Vec<u8> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(input).unwrap();
encoder.finish().unwrap()
}
#[tokio::test]
async fn uppercase_gzip_decompressed() {
let content = "uppercase GZIP test";
let compressed = gzip_compress(content.as_bytes());
let addr = raw_streaming_server(move |_req, mut stream| {
let compressed = compressed.clone();
async move {
let response = format!(
"HTTP/1.1 200 OK\r\n\
Content-Encoding: GZIP\r\n\
Content-Length: {}\r\n\
\r\n",
compressed.len()
);
stream.write_all(response.as_bytes()).await.unwrap();
stream.write_all(&compressed).await.unwrap();
stream.flush().await.unwrap();
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.text().await.unwrap(), content);
}
#[tokio::test]
async fn mixed_case_gzip_decompressed() {
let content = "mixed case Gzip test";
let compressed = gzip_compress(content.as_bytes());
let addr = raw_streaming_server(move |_req, mut stream| {
let compressed = compressed.clone();
async move {
let response = format!(
"HTTP/1.1 200 OK\r\n\
Content-Encoding: Gzip\r\n\
Content-Length: {}\r\n\
\r\n",
compressed.len()
);
stream.write_all(response.as_bytes()).await.unwrap();
stream.write_all(&compressed).await.unwrap();
stream.flush().await.unwrap();
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.text().await.unwrap(), content);
}
#[tokio::test]
async fn x_gzip_alias_decompressed() {
let content = "x-gzip alias test";
let compressed = gzip_compress(content.as_bytes());
let addr = raw_streaming_server(move |_req, mut stream| {
let compressed = compressed.clone();
async move {
let response = format!(
"HTTP/1.1 200 OK\r\n\
Content-Encoding: x-gzip\r\n\
Content-Length: {}\r\n\
\r\n",
compressed.len()
);
stream.write_all(response.as_bytes()).await.unwrap();
stream.write_all(&compressed).await.unwrap();
stream.flush().await.unwrap();
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.text().await.unwrap(), content);
}
}
#[cfg(feature = "deflate")]
#[tokio::test]
async fn uppercase_deflate_decompressed() {
use flate2::Compression;
use flate2::write::ZlibEncoder;
use std::io::Write;
use tokio::io::AsyncWriteExt;
let content = "uppercase DEFLATE test";
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(content.as_bytes()).unwrap();
let compressed = encoder.finish().unwrap();
let addr = raw_streaming_server(move |_req, mut stream| {
let compressed = compressed.clone();
async move {
let response = format!(
"HTTP/1.1 200 OK\r\n\
Content-Encoding: DEFLATE\r\n\
Content-Length: {}\r\n\
\r\n",
compressed.len()
);
stream.write_all(response.as_bytes()).await.unwrap();
stream.write_all(&compressed).await.unwrap();
stream.flush().await.unwrap();
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.text().await.unwrap(), content);
}
#[cfg(feature = "brotli")]
#[tokio::test]
async fn uppercase_brotli_decompressed() {
use std::io::Write;
use tokio::io::AsyncWriteExt;
let content = "uppercase BR test";
let mut compressed = Vec::new();
{
let mut writer = brotli::CompressorWriter::new(&mut compressed, 4096, 6, 22);
writer.write_all(content.as_bytes()).unwrap();
}
let addr = raw_streaming_server(move |_req, mut stream| {
let compressed = compressed.clone();
async move {
let response = format!(
"HTTP/1.1 200 OK\r\n\
Content-Encoding: BR\r\n\
Content-Length: {}\r\n\
\r\n",
compressed.len()
);
stream.write_all(response.as_bytes()).await.unwrap();
stream.write_all(&compressed).await.unwrap();
stream.flush().await.unwrap();
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.text().await.unwrap(), content);
}
#[cfg(feature = "zstd")]
#[tokio::test]
async fn uppercase_zstd_decompressed() {
use tokio::io::AsyncWriteExt;
let content = "uppercase ZSTD test";
let compressed = zstd::encode_all(content.as_bytes(), 3).unwrap();
let addr = raw_streaming_server(move |_req, mut stream| {
let compressed = compressed.clone();
async move {
let response = format!(
"HTTP/1.1 200 OK\r\n\
Content-Encoding: ZSTD\r\n\
Content-Length: {}\r\n\
\r\n",
compressed.len()
);
stream.write_all(response.as_bytes()).await.unwrap();
stream.write_all(&compressed).await.unwrap();
stream.flush().await.unwrap();
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.text().await.unwrap(), content);
}
#[cfg(feature = "gzip")]
#[tokio::test]
async fn malformed_gzip_body_returns_error() {
use tokio::io::AsyncWriteExt;
let addr = raw_streaming_server(move |_req, mut stream| async move {
let body = b"this is not valid gzip compressed data";
let header = format!(
"HTTP/1.1 200 OK\r\nContent-Encoding: gzip\r\nContent-Length: {}\r\n\r\n",
body.len()
);
stream.write_all(header.as_bytes()).await.unwrap();
stream.write_all(body).await.unwrap();
stream.flush().await.unwrap();
stream.shutdown().await.unwrap();
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
let result = resp.text().await;
assert!(
result.is_err(),
"malformed gzip body should cause decompression error, got: {:?}",
result.ok()
);
}
#[tokio::test]
async fn content_length_mismatch_too_long_poisons_reuse() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let conn_count = Arc::new(AtomicUsize::new(0));
tokio::spawn({
let conn_count = conn_count.clone();
async move {
loop {
let (mut stream, _) = match listener.accept().await {
Ok(v) => v,
Err(_) => continue,
};
conn_count.fetch_add(1, Ordering::SeqCst);
tokio::spawn(async move {
let mut buf = [0u8; 4096];
let n = match stream.read(&mut buf).await {
Ok(0) | Err(_) => return,
Ok(n) => n,
};
if !buf[..n].windows(4).any(|w| w == b"\r\n\r\n") {
return;
}
let extra = "x".repeat(100);
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: keep-alive\r\n\r\nhello{extra}"
);
let _ = stream.write_all(response.as_bytes()).await;
let _ = stream.flush().await;
let _ = tokio::time::timeout(Duration::from_millis(300), stream.read(&mut buf))
.await;
});
}
}
});
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.pool_idle_timeout(Duration::from_secs(60))
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let url = format!("http://{addr}/");
let resp = client.get(&url).unwrap().send().await.unwrap();
let body = resp.text().await.unwrap();
assert_eq!(body, "hello");
tokio::time::sleep(Duration::from_millis(50)).await;
let before = conn_count.load(Ordering::SeqCst);
let result = client.get(&url).unwrap().send().await;
match result {
Ok(resp) => {
let _ = resp.text().await.unwrap();
let after = conn_count.load(Ordering::SeqCst);
assert!(
after > before,
"expected a new connection after Content-Length \
mismatch (before={before}, after={after})"
);
}
Err(_) => {
}
}
}
#[cfg(feature = "gzip")]
#[tokio::test]
async fn content_length_removed_after_decompression() {
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
let content = "content length should disappear";
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(content.as_bytes()).unwrap();
let compressed = encoder.finish().unwrap();
let (addr, _counter) = h1_server_with(move |_req| {
let compressed = compressed.clone();
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "gzip")
.header("content-length", compressed.len().to_string())
.body(Full::new(Bytes::from(compressed)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert!(
resp.headers().get("content-length").is_none(),
"Content-Length should be stripped after decompression"
);
let text = resp.text().await.unwrap();
assert_eq!(text, content);
}
#[cfg(feature = "gzip")]
#[tokio::test]
async fn decompressed_body_empty_is_ok() {
use flate2::Compression;
use flate2::write::GzEncoder;
let encoder = GzEncoder::new(Vec::new(), Compression::default());
let compressed = encoder.finish().unwrap();
let (addr, _counter) = h1_server_with(move |_req| {
let compressed = compressed.clone();
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "gzip")
.body(Full::new(Bytes::from(compressed)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
let text = resp.text().await.unwrap();
assert_eq!(
text, "",
"empty gzip body should decompress to empty string"
);
}
#[cfg(feature = "gzip")]
#[tokio::test]
async fn decompress_gzip_round_trip_with_large_body() {
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
let content = "A".repeat(65536); let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(content.as_bytes()).unwrap();
let compressed = encoder.finish().unwrap();
let (addr, _counter) = h1_server_with(move |_req| {
let compressed = compressed.clone();
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "gzip")
.header("content-length", compressed.len().to_string())
.body(Full::new(Bytes::from(compressed)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
let text = resp.text().await.unwrap();
assert_eq!(text, content);
}
#[cfg(feature = "gzip")]
#[tokio::test]
async fn trailer_frame_passes_through_decompress() {
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
use tokio::io::AsyncWriteExt;
use aioduct::observer::{ConnectionEvent, RequestEvent, RequestObserver, RequestPhase};
let content = "hello trailer decompress";
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
encoder.write_all(content.as_bytes()).unwrap();
let compressed = encoder.finish().unwrap();
let events: Arc<Mutex<Vec<RequestPhase>>> = Arc::new(Mutex::new(Vec::new()));
let events_clone = events.clone();
struct TrailerObserver(Arc<Mutex<Vec<RequestPhase>>>);
impl RequestObserver for TrailerObserver {
fn on_event(&self, event: &RequestEvent) {
self.0.lock().unwrap().push(event.phase.clone());
}
fn on_connection_event(&self, _event: &ConnectionEvent) {}
}
let addr = raw_streaming_server(move |_req, mut stream| {
let compressed = compressed.clone();
async move {
let chunk_header = format!("{:x}\r\n", compressed.len());
let response_header = "HTTP/1.1 200 OK\r\n\
Content-Encoding: gzip\r\n\
Transfer-Encoding: chunked\r\n\
Trailer: x-response-time\r\n\
\r\n";
stream.write_all(response_header.as_bytes()).await.unwrap();
stream.write_all(chunk_header.as_bytes()).await.unwrap();
stream.write_all(&compressed).await.unwrap();
stream.write_all(b"\r\n").await.unwrap();
stream.write_all(b"0\r\n").await.unwrap();
stream.write_all(b"x-response-time: 42\r\n").await.unwrap();
stream.write_all(b"\r\n").await.unwrap();
stream.flush().await.unwrap();
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.timeout(Duration::from_secs(5))
.request_observer(TrailerObserver(events_clone))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
let mut stream = resp.into_bytes_stream();
let mut body = Vec::new();
while let Some(chunk) = stream.next().await {
body.extend_from_slice(&chunk.unwrap());
}
assert_eq!(
String::from_utf8(body).unwrap(),
content,
"decompressed body must match original"
);
let captured = events.lock().unwrap();
let has_trailers = captured.iter().any(|p| {
matches!(p, RequestPhase::TrailersReceived { headers }
if headers.iter().any(|(k, v)| k == "x-response-time" && v == "42"))
});
assert!(
has_trailers,
"expected TrailersReceived with x-response-time: 42, got: {captured:?}"
);
}
#[cfg(feature = "brotli")]
#[tokio::test]
async fn decompress_brotli_round_trip() {
use std::io::Write;
let content = "hello brotli round trip test payload with sufficient length to compress well";
let mut compressed = Vec::new();
{
let mut writer = brotli::CompressorWriter::new(&mut compressed, 4096, 6, 22);
writer.write_all(content.as_bytes()).unwrap();
drop(writer);
}
let (addr, _counter) = h1_server_with(move |_req| {
let compressed = compressed.clone();
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "br")
.body(Full::new(Bytes::from(compressed)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
let text = resp.text().await.unwrap();
assert_eq!(text, content);
}
#[cfg(feature = "zstd")]
#[tokio::test]
async fn decompress_zstd_round_trip() {
let content = "hello zstd round trip test payload with sufficient length to compress well";
let compressed = zstd::encode_all(content.as_bytes(), 3).unwrap();
let (addr, _counter) = h1_server_with(move |_req| {
let compressed = compressed.clone();
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "zstd")
.body(Full::new(Bytes::from(compressed)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
let text = resp.text().await.unwrap();
assert_eq!(text, content);
}
#[cfg(feature = "gzip")]
#[tokio::test]
async fn corrupt_gzip_body_propagates_decode_error() {
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
let valid_content = "second request valid content";
let valid = {
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(valid_content.as_bytes()).unwrap();
e.finish().unwrap()
};
let corrupt_original =
"first request content that will be corrupted in the middle of the gzip stream";
let mut corrupt = {
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(corrupt_original.as_bytes()).unwrap();
e.finish().unwrap()
};
let start = corrupt.len() / 3;
let end = (corrupt.len() * 2) / 3;
for byte in &mut corrupt[start..end] {
*byte = 0;
}
let request_count = Arc::new(AtomicU32::new(0));
let (addr, _counter) = h1_server_with({
let request_count = request_count.clone();
let valid = valid.clone();
let corrupt = corrupt.clone();
move |_req: Request<hyper::body::Incoming>| {
let count = request_count.fetch_add(1, Ordering::SeqCst);
let body = if count == 0 {
corrupt.clone()
} else {
valid.clone()
};
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "gzip")
.body(Full::new(Bytes::from(body)))
.unwrap(),
)
}
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
let result = resp.text().await;
assert!(
result.is_err(),
"corrupt gzip body must produce a decode error, got: {:?}",
result.ok()
);
let resp2 = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
let text2 = resp2.text().await.unwrap();
assert_eq!(text2, valid_content);
}
#[cfg(all(feature = "gzip", feature = "brotli"))]
#[tokio::test]
async fn content_encoding_brotli_with_gzip_body_errors() {
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(b"gzip body served as brotli").unwrap();
let gzip_body = encoder.finish().unwrap();
let (addr, _counter) = h1_server_with(move |_req| {
let gzip_body = gzip_body.clone();
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "br")
.body(Full::new(Bytes::from(gzip_body)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::new();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
let result = resp.text().await;
assert!(
result.is_err(),
"brotli Content-Encoding with gzip body must error, got: {:?}",
result.ok()
);
}
#[cfg(feature = "gzip")]
#[tokio::test]
async fn gzip_bomb_rejected_by_max_decoded_size() {
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
let big = vec![0u8; 100_000_000]; let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&big).unwrap();
let compressed = encoder.finish().unwrap();
let (addr, _counter) = h1_server_with(move |_req| {
let compressed = compressed.clone();
async move {
Ok::<_, Infallible>(
Response::builder()
.header("content-encoding", "gzip")
.header("content-length", compressed.len().to_string())
.body(Full::new(Bytes::from(compressed)))
.unwrap(),
)
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.max_decoded_size(Some(1_000_000)) .timeout(Duration::from_secs(10))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
let result = resp.text().await;
assert!(
result.is_err(),
"decompression bomb must be rejected by max_decoded_size limit"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("exceeds max size"),
"error should mention max size, got: {err_msg}"
);
}