fix(fetch): request identity encoding for HTTP ranges

This commit is contained in:
ldm0
2026-09-24 20:01:11 +08:00
parent 1cff323bf2
commit 2edc2e4bd9
3 changed files with 175 additions and 0 deletions
@@ -6096,6 +6096,8 @@ fetch/content-length/content-length.html
fetch/nosniff/importscripts.html
fetch/range/blob.any.js?moli-wpt-any=dedicatedworker
fetch/range/blob.any.js?moli-wpt-any=window
fetch/range/general.any.js?moli-wpt-any=dedicatedworker
fetch/range/general.any.js?moli-wpt-any=window
fetch/security/dangling-markup/media.html
fetch/security/dangling-markup/option.html
fetch/security/dangling-markup/textarea.html
+6
View File
@@ -197,6 +197,12 @@ fn outgoing_request_header_bytes_for_url(
outgoing.push((name.clone(), value.clone()));
}
// Fetch selects identity whenever Range is present, even with an invalid
// value. Explicit embedder encoding preferences still take precedence.
if header_present(&outgoing, "range") {
append_header_if_missing(&mut outgoing, "Accept-Encoding", "identity".to_owned());
}
if request.has_browser_identity_override() {
append_encoded_header_if_missing(
&mut outgoing,
+167
View File
@@ -357,3 +357,170 @@ async fn current_hop_headers_expire_before_redirect_method_rewrite_in_all_transp
}
Ok(())
}
fn accept_encoding_values(request: &[u8]) -> Vec<&str> {
std::str::from_utf8(request)
.unwrap()
.lines()
.filter_map(|line| {
let (name, value) = line.split_once(':')?;
name.eq_ignore_ascii_case("accept-encoding")
.then_some(value.trim())
})
.collect()
}
#[tokio::test]
async fn range_requests_select_identity_in_every_transport() -> Result<()> {
// Presence, including an empty or malformed value, selects identity.
// Ordinary requests before and after must still negotiate compression.
let ranges = [
None,
Some("bytes=0-10"),
Some("foo=0-10"),
Some("foo"),
Some(""),
Some("bytes=-3"),
Some("bytes=0-1,4-5"),
None,
];
let listener = TcpListener::bind("127.0.0.1:0").await?;
let url = format!("http://{}/range", listener.local_addr()?);
let server = tokio::spawn(serve_responses(
listener,
vec![response(b""); ranges.len() * 3],
));
let client = FetchClient::new(&FetchConfig::default(), new_shared_browser_cookie_store());
for mode in ["buffered", "html", "raw"] {
for range in ranges {
let headers = range
.map(|value| vec![("rAnGe".into(), value.into())])
.unwrap_or_default();
let recorder = NetworkObservationRecorder::default();
let request = Request::new("GET", &url, None, headers)?
.with_network_observation_recorder(recorder.clone());
fetch_in_mode(&client, request.clone(), mode).await?;
assert!(
request
.request_headers
.iter()
.all(|(name, _)| !name.eq_ignore_ascii_case("accept-encoding"))
);
if range.is_some() {
let journal = recorder.snapshot();
assert!(
journal
.final_request_observation()
.unwrap()
.headers()
.iter()
.any(|(name, value)| name.eq_ignore_ascii_case("accept-encoding")
&& value == "identity"),
"{mode}: {range:?}"
);
}
}
}
let requests = server.await??;
assert_eq!(requests.len(), ranges.len() * 3);
for (request, range) in requests.iter().zip(ranges.into_iter().cycle()) {
let values = accept_encoding_values(request);
if range.is_some() {
assert_eq!(values, ["identity"], "{range:?}");
} else {
assert_eq!(values.len(), 1);
assert!(values[0].contains("gzip"), "{values:?}");
}
}
assert!(client.shutdown().is_clean());
Ok(())
}
#[tokio::test]
async fn range_requests_respect_native_header_configuration() -> Result<()> {
for (defaults, headers, expected) in [
(vec![("Range", "bytes=0-10")], vec![], "identity"),
(
vec![("aCcEpT-EnCoDiNg", "gzip")],
vec![("Range", "bytes=0-10")],
"gzip",
),
(
vec![("Range", "bytes=0-10")],
vec![("Accept-Encoding", "br")],
"br",
),
(
vec![("Range", "bytes=0-10")],
vec![("Accept-Encoding", "")],
"",
),
] {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let url = format!("http://{}/configured", listener.local_addr()?);
let server = tokio::spawn(serve_responses(listener, vec![response(b""); 3]));
let mut config = FetchConfig::default();
for (name, value) in defaults {
config.push_default_request_header(name, value);
}
let client = FetchClient::new(&config, new_shared_browser_cookie_store());
for mode in ["buffered", "html", "raw"] {
let request = Request::new(
"GET",
&url,
None,
headers
.iter()
.map(|(n, v)| (n.to_string(), v.to_string()))
.collect(),
)?;
fetch_in_mode(&client, request, mode).await?;
}
for request in server.await?? {
assert_eq!(accept_encoding_values(&request), [expected]);
}
assert!(client.shutdown().is_clean());
}
Ok(())
}
#[tokio::test]
async fn range_redirects_keep_identity_and_response_decompression() -> Result<()> {
// A native HTTP caller still receives decoded bytes if a server ignores
// the requested encoding and sends a complete gzip response.
let gzip = b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\x4b\x49\x4d\xce\x4f\x49\x4d\x01\x00\xf6\x9a\xf0\x1a\x07\x00\x00\x00";
let compressed = [
format!("HTTP/1.1 200 OK\r\nContent-Encoding: gzip\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", gzip.len()).as_bytes(),
gzip,
].concat();
let redirect = b"HTTP/1.1 307 Redirect\r\nLocation: /final\r\nContent-Length: 0\r\nConnection: close\r\n\r\n".to_vec();
for mode in ["html", "raw"] {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let url = format!("http://{}/start", listener.local_addr()?);
let server = tokio::spawn(serve_responses(
listener,
vec![redirect.clone(), compressed.clone(), compressed.clone()],
));
let client = FetchClient::new(&FetchConfig::default(), new_shared_browser_cookie_store());
let request = Request::new(
"GET",
&url,
None,
vec![("Range".into(), "bytes=0-10".into())],
)?;
let (head, body) = fetch_in_mode(&client, request, mode).await?;
assert!(head.redirected);
assert_eq!(head.final_url.path(), "/final");
assert_eq!(body, b"decoded");
let (_, body) = fetch_in_mode(&client, Request::get(&url)?, mode).await?;
assert_eq!(body, b"decoded");
let requests = server.await??;
assert_eq!(requests.len(), 3);
for request in &requests[..2] {
assert_eq!(accept_encoding_values(request), ["identity"]);
}
assert!(accept_encoding_values(&requests[2])[0].contains("gzip"));
assert!(client.shutdown().is_clean());
}
Ok(())
}