Skip to content
Merged
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
4 changes: 4 additions & 0 deletions crates/rttp-client/src/response/raw_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,10 @@ impl Parser {
let mut decoder = flate2::read::GzDecoder::new(binary.as_slice());
let mut buffer = Vec::new();
decoder.read_to_end(&mut buffer).map_err(error::decode)?;
response.headers.retain(|header| {
!header.name().eq_ignore_ascii_case("Content-Encoding")
&& !header.name().eq_ignore_ascii_case("Content-Length")
});
let body = ResponseBody::new(buffer);
response.body(body);
return Ok(());
Expand Down
33 changes: 33 additions & 0 deletions crates/rttp-client/tests/test_http_async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ use std::net::TcpListener;
#[cfg(feature = "async")]
use std::thread;

#[cfg(feature = "async")]
fn gzip_bytes(bytes: &[u8]) -> Vec<u8> {
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
encoder.write_all(bytes).expect("write gzip fixture");
encoder.finish().expect("finish gzip fixture")
}

#[cfg(feature = "async")]
fn client() -> HttpClient {
HttpClient::new()
Expand Down Expand Up @@ -111,6 +118,32 @@ fn test_async_http() {
});
}

#[test]
#[cfg(feature = "async")]
fn test_async_buffered_gzip_response_exposes_decoded_body_headers() {
let body = gzip_bytes(b"decoded");
let mut raw = format!(
"HTTP/1.1 200 OK\r\nContent-Encoding: gzip\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
)
.into_bytes();
raw.extend_from_slice(&body);
let (addr, _handle) = support::spawn_chunked_response_server(raw);

block_on(async {
let response = client()
.get()
.url(format!("http://{}/gzip", addr))
.rasync()
.await
.expect("async buffered gzip response");

assert_eq!(b"decoded", response.body().binary());
assert!(response.header("Content-Encoding").is_none());
assert!(response.header("Content-Length").is_none());
});
}

#[test]
#[cfg(feature = "async")]
fn test_async_head_response_is_bodyless_and_preserves_headers() {
Expand Down
30 changes: 30 additions & 0 deletions crates/rttp-client/tests/test_http_basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use std::net::TcpListener;
use std::thread;
use std::time::Duration;

use flate2::write::GzEncoder;
use flate2::Compression;
use rttp_client::types::{Auth, Para, Proxy, RoUrl};
use rttp_client::ConnectionReader;
use rttp_client::{Config, HttpClient};
Expand All @@ -15,6 +17,12 @@ fn client() -> HttpClient {
HttpClient::new()
}

fn gzip_bytes(bytes: &[u8]) -> Vec<u8> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(bytes).expect("write gzip fixture");
encoder.finish().expect("finish gzip fixture")
}

fn spawn_streaming_upload_capture_server() -> (std::net::SocketAddr, thread::JoinHandle<Vec<u8>>) {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind upload capture server");
let addr = listener.local_addr().expect("upload capture addr");
Expand Down Expand Up @@ -230,6 +238,28 @@ fn test_gzip() {
assert!(response.is_ok());
}

#[test]
fn test_buffered_gzip_response_exposes_decoded_body_headers() {
let body = gzip_bytes(b"decoded");
let mut raw = format!(
"HTTP/1.1 200 OK\r\nContent-Encoding: gzip\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
)
.into_bytes();
raw.extend_from_slice(&body);
let (addr, _handle) = support::spawn_chunked_response_server(raw);

let response = client()
.get()
.url(format!("http://{}/gzip", addr))
.emit()
.expect("buffered gzip response");

assert_eq!(b"decoded", response.body().binary());
assert!(response.header("Content-Encoding").is_none());
assert!(response.header("Content-Length").is_none());
}

#[test]
fn test_invalid_gzip_returns_error_instead_of_panicking() {
let (addr, _handle) = support::spawn_invalid_gzip_server();
Expand Down
74 changes: 69 additions & 5 deletions crates/rttp-client/tests/test_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1344,14 +1344,50 @@ fn test_content_encoding_runtime_decodes_only_single_supported_gzip_coding() {
.expect("single gzip response should decode");

assert_eq!("OK", response.body().string().unwrap());
assert!(response.header("Content-Encoding").is_none());
assert!(response.header("Content-Length").is_none());
assert!(response.content_encoding().unwrap().is_none());
assert!(response
.binary()
.windows(b"Content-Encoding: gzip".len())
.any(|window| window == b"Content-Encoding: gzip"));
}

#[test]
fn test_content_encoding_runtime_leaves_empty_gzip_body_and_headers_unchanged() {
let raw = concat!(
"HTTP/1.1 200 OK\r\n",
"Content-Encoding: gzip\r\n",
"Content-Length: 0\r\n",
"\r\n"
);
let response = Response::new(RoUrl::with("https://example.test"), raw.as_bytes().to_vec())
.expect("empty gzip response should remain usable");

assert!(response.body().binary().is_empty());
assert_eq!(
vec!["gzip"],
Some("gzip"),
response
.content_encoding()
.expect("content-encoding should parse")
.expect("content-encoding should be present")
.codings()
.header_value("Content-Encoding")
.map(String::as_str)
);
assert_eq!(
Some("0"),
response.header_value("Content-Length").map(String::as_str)
);
}

#[test]
fn test_content_encoding_runtime_rejects_malformed_single_gzip_body() {
let raw = concat!(
"HTTP/1.1 200 OK\r\n",
"Content-Encoding: gzip\r\n",
"Content-Length: 8\r\n",
"\r\n",
"not-gzip"
);

assert!(Response::new(RoUrl::with("https://example.test"), raw.as_bytes().to_vec()).is_err());
}

#[test]
Expand All @@ -1377,6 +1413,34 @@ fn test_content_encoding_runtime_leaves_stacked_or_unsupported_codings_undecoded
);
}

#[test]
fn test_content_encoding_runtime_leaves_duplicate_gzip_fields_undecoded() {
let body = gzip_bytes(b"OK");
let mut raw = concat!(
"HTTP/1.1 200 OK\r\n",
"Content-Encoding: gzip\r\n",
"Content-Encoding: gzip\r\n"
)
.as_bytes()
.to_vec();
raw.extend_from_slice(format!("Content-Length: {}\r\n\r\n", body.len()).as_bytes());
raw.extend_from_slice(&body);

let response = Response::new(RoUrl::with("https://example.test"), raw)
.expect("duplicate gzip fields should remain usable");
let content_length = body.len().to_string();

assert_eq!(body, response.body().binary());
assert_eq!(
vec![&"gzip".to_string(), &"gzip".to_string()],
response.header_values("Content-Encoding")
);
assert_eq!(
Some(content_length.as_str()),
response.header_value("Content-Length").map(String::as_str)
);
}

#[test]
fn test_parse_content_encoding_rejects_invalid_duplicate_and_excessive_values() {
for value in [
Expand Down
62 changes: 41 additions & 21 deletions tests/http11_client_server_matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,12 @@ fn assert_response_content_encoding(
.into_iter()
.map(String::as_str)
.collect();
if expected.values == ["gzip"] {
assert!(raw_values.is_empty(), "{name}");
assert!(response.header("content-length").is_none(), "{name}");
assert!(response.content_encoding().unwrap().is_none(), "{name}");
return;
}
assert_eq!(expected.values, raw_values.as_slice(), "{name}");

let content_encoding = response
Expand Down Expand Up @@ -2439,27 +2445,41 @@ fn sync_client_observes_rttp_server_content_encoding_helpers() {
.emit()
.unwrap_or_else(|err| panic!("{} response should parse: {err}", case.name));

let expected_header_value = case.codings.join(", ");
assert_eq!(
vec![expected_header_value.as_str()],
response
.header_values("Content-Encoding")
.into_iter()
.map(String::as_str)
.collect::<Vec<_>>(),
"{}",
case.name
);
let content_encoding = response
.content_encoding()
.unwrap_or_else(|err| panic!("{} Content-Encoding should parse: {err}", case.name))
.unwrap_or_else(|| panic!("{} should include Content-Encoding", case.name));
assert_eq!(
case.codings,
content_encoding.codings().as_slice(),
"{}",
case.name
);
if case.codings == ["gzip"] {
assert!(
response.header("Content-Encoding").is_none(),
"{}",
case.name
);
assert!(response.header("Content-Length").is_none(), "{}", case.name);
assert!(
response.content_encoding().unwrap().is_none(),
"{}",
case.name
);
} else {
let expected_header_value = case.codings.join(", ");
assert_eq!(
vec![expected_header_value.as_str()],
response
.header_values("Content-Encoding")
.into_iter()
.map(String::as_str)
.collect::<Vec<_>>(),
"{}",
case.name
);
let content_encoding = response
.content_encoding()
.unwrap_or_else(|err| panic!("{} Content-Encoding should parse: {err}", case.name))
.unwrap_or_else(|| panic!("{} should include Content-Encoding", case.name));
assert_eq!(
case.codings,
content_encoding.codings().as_slice(),
"{}",
case.name
);
}
assert_eq!("OK", response.body().string().unwrap(), "{}", case.name);

handle.join().expect("content-encoding server thread");
Expand Down