Fix responses with more than 100 headers failing on HTTP/1 (#658)

This commit is contained in:
Gregory Schier
2026-09-14 21:04:47 -07:00
committed by GitHub
parent 7c660f4528
commit fd95b4cba3
6 changed files with 151 additions and 80 deletions
Generated
+34 -74
View File
@@ -685,6 +685,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]] [[package]]
name = "base64-simd" name = "base64-simd"
version = "0.8.0" version = "0.8.0"
@@ -2942,7 +2948,7 @@ dependencies = [
"libc", "libc",
"libgit2-sys", "libgit2-sys",
"log 0.4.29", "log 0.4.29",
"openssl-probe", "openssl-probe 0.1.6",
"openssl-sys", "openssl-sys",
"url", "url",
] ]
@@ -4471,17 +4477,17 @@ dependencies = [
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.14" version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
dependencies = [ dependencies = [
"libc", "libc",
"log 0.4.29", "log 0.4.29",
"openssl", "openssl",
"openssl-probe", "openssl-probe 0.2.1",
"openssl-sys", "openssl-sys",
"schannel", "schannel",
"security-framework 2.11.1", "security-framework 3.5.1",
"security-framework-sys", "security-framework-sys",
"tempfile", "tempfile",
] ]
@@ -5055,6 +5061,12 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]] [[package]]
name = "openssl-src" name = "openssl-src"
version = "300.6.1+3.6.3" version = "300.6.1+3.6.3"
@@ -6717,12 +6729,11 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.20" version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029"
dependencies = [ dependencies = [
"async-compression", "base64 0.23.1",
"base64 0.22.1",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@@ -6744,6 +6755,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"rustls-platform-verifier",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
@@ -6758,46 +6770,7 @@ dependencies = [
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams 0.4.2", "wasm-streams",
"web-sys",
]
[[package]]
name = "reqwest"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log 0.4.29",
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-util",
"tower 0.5.2",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams 0.5.0",
"web-sys", "web-sys",
] ]
@@ -7431,7 +7404,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
dependencies = [ dependencies = [
"openssl-probe", "openssl-probe 0.1.6",
"rustls-pki-types", "rustls-pki-types",
"schannel", "schannel",
"security-framework 3.5.1", "security-framework 3.5.1",
@@ -7517,11 +7490,11 @@ dependencies = [
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.27" version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -8596,7 +8569,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"plist", "plist",
"raw-window-handle", "raw-window-handle",
"reqwest 0.13.3", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr", "serde_repr",
@@ -8886,7 +8859,7 @@ dependencies = [
"minisign-verify", "minisign-verify",
"osakit", "osakit",
"percent-encoding", "percent-encoding",
"reqwest 0.13.3", "reqwest",
"rustls", "rustls",
"semver", "semver",
"serde", "serde",
@@ -10204,19 +10177,6 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "wasm-streams"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]] [[package]]
name = "wasm-streams" name = "wasm-streams"
version = "0.5.0" version = "0.5.0"
@@ -11240,7 +11200,7 @@ name = "yaak-api"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"log 0.4.29", "log 0.4.29",
"reqwest 0.12.20", "reqwest",
"sysproxy", "sysproxy",
"thiserror 2.0.17", "thiserror 2.0.17",
"yaak-common", "yaak-common",
@@ -11265,7 +11225,7 @@ dependencies = [
"r2d2", "r2d2",
"r2d2_sqlite", "r2d2_sqlite",
"rand 0.9.5", "rand 0.9.5",
"reqwest 0.12.20", "reqwest",
"rlimit", "rlimit",
"serde", "serde",
"serde_json", "serde_json",
@@ -11351,7 +11311,7 @@ dependencies = [
"oxc_resolver", "oxc_resolver",
"predicates", "predicates",
"rand 0.8.7", "rand 0.8.7",
"reqwest 0.12.20", "reqwest",
"rolldown", "rolldown",
"schemars 0.8.22", "schemars 0.8.22",
"serde", "serde",
@@ -11515,7 +11475,7 @@ dependencies = [
"log 0.4.29", "log 0.4.29",
"mime_guess", "mime_guess",
"native-tls", "native-tls",
"reqwest 0.12.20", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"tempfile", "tempfile",
@@ -11538,7 +11498,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"log 0.4.29", "log 0.4.29",
"reqwest 0.12.20", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
@@ -11611,7 +11571,7 @@ dependencies = [
"md5 0.7.0", "md5 0.7.0",
"path-slash", "path-slash",
"rand 0.9.5", "rand 0.9.5",
"reqwest 0.12.20", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
+8 -1
View File
@@ -47,7 +47,14 @@ chrono = "0.4.42"
hex = "0.4.3" hex = "0.4.3"
keyring = "3.6.3" keyring = "3.6.3"
log = "0.4.29" log = "0.4.29"
reqwest = "0.12.20" reqwest = { version = "0.13.5", default-features = false, features = [
# reqwest 0.13 switched `default-tls` from native-tls to rustls; keep the
# rest of the old default set and stay on native-tls.
"charset",
"http2",
"system-proxy",
"native-tls",
] }
rustls = { version = "0.23.34", default-features = false } rustls = { version = "0.23.34", default-features = false }
rustls-platform-verifier = "0.6.2" rustls-platform-verifier = "0.6.2"
schemars = { version = "0.8.22", features = ["chrono"] } schemars = { version = "0.8.22", features = ["chrono"] }
+1 -1
View File
@@ -22,7 +22,7 @@ include_dir = "0.7"
keyring = { workspace = true, features = ["apple-native", "windows-native", "sync-secret-service"] } keyring = { workspace = true, features = ["apple-native", "windows-native", "sync-secret-service"] }
log = { workspace = true } log = { workspace = true }
rand = "0.8" rand = "0.8"
reqwest = { workspace = true } reqwest = { workspace = true, features = ["form"] }
rolldown = "0.1.0" rolldown = "0.1.0"
oxc_resolver = "=11.10.0" oxc_resolver = "=11.10.0"
schemars = { workspace = true } schemars = { workspace = true }
+2 -1
View File
@@ -48,7 +48,8 @@ reqwest = { workspace = true, features = [
"brotli", "brotli",
"deflate", "deflate",
"json", "json",
"rustls-tls-manual-roots-no-provider", "query",
"rustls-no-provider",
"socks", "socks",
"http2", "http2",
] } ] }
+2 -2
View File
@@ -20,9 +20,8 @@ log = { workspace = true }
mime_guess = "2.0.5" mime_guess = "2.0.5"
native-tls = { version = "0.2", features = ["alpn"] } native-tls = { version = "0.2", features = ["alpn"] }
reqwest = { workspace = true, features = [ reqwest = { workspace = true, features = [
"rustls-tls-manual-roots-no-provider", "rustls-no-provider",
"native-tls", "native-tls",
"native-tls-alpn",
"socks", "socks",
"http2", "http2",
"stream", "stream",
@@ -41,3 +40,4 @@ yaak-tls = { workspace = true }
[dev-dependencies] [dev-dependencies]
tempfile = "3" tempfile = "3"
tokio = { workspace = true, features = ["net"] }
+104 -1
View File
@@ -10,8 +10,14 @@ use yaak_tls::{
pub const HTTP2_MAX_RESPONSE_HEADER_LIST_SIZE: u32 = 1024 * 1024; pub const HTTP2_MAX_RESPONSE_HEADER_LIST_SIZE: u32 = 1024 * 1024;
/// hyper caps HTTP/1 responses at 100 headers, which real servers exceed.
/// https://yaak.app/feedback/posts/when-response-headers-exceed-a-certain-count-hyper-throws-error
pub const HTTP1_MAX_RESPONSE_HEADERS: usize = 1024;
fn client_builder() -> ClientBuilder { fn client_builder() -> ClientBuilder {
Client::builder().http2_max_header_list_size(HTTP2_MAX_RESPONSE_HEADER_LIST_SIZE) Client::builder()
.http2_max_header_list_size(HTTP2_MAX_RESPONSE_HEADER_LIST_SIZE)
.http1_max_headers(HTTP1_MAX_RESPONSE_HEADERS)
} }
#[derive(Clone)] #[derive(Clone)]
@@ -399,3 +405,100 @@ UFNAXPsoutUompC1Z57bKA6OKM2hRANCAATCYYKhzgHEaRaGsYVjJSoXvoroL8qe
assert!(build_native_tls_identity(Some(empty)).unwrap().is_none()); assert!(build_native_tls_identity(Some(empty)).unwrap().is_none());
} }
} }
#[cfg(test)]
mod header_limit_tests {
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
/// Accepts one connection and replies with a response head of exactly
/// `total_headers` fields. The trailing `Content-Length` is one of them,
/// because hyper counts every field against its limit.
async fn serve_response_with_header_count(total_headers: usize) -> String {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
// Read the request head so the client is not writing into a closed socket
let mut buf = [0u8; 4096];
loop {
let n = socket.read(&mut buf).await.unwrap();
if n == 0 || buf[..n].windows(4).any(|w| w == b"\r\n\r\n") {
break;
}
}
let mut response = String::from("HTTP/1.1 200 OK\r\n");
for i in 0..total_headers - 1 {
response.push_str(&format!("X-Test-{i}: value-{i}\r\n"));
}
response.push_str("Content-Length: 0\r\n\r\n");
// A response the client rejects mid-parse closes the socket under
// us, so a failed write here is an expected outcome, not a fault
let _ = socket.write_all(response.as_bytes()).await;
let _ = socket.flush().await;
});
format!("http://{addr}/")
}
fn options() -> HttpConnectionOptions {
HttpConnectionOptions {
id: "test".to_string(),
validate_certificates: true,
http_version: HttpVersion::Http1,
proxy: HttpConnectionProxySetting::Disabled,
client_certificate: None,
dns_overrides: Vec::new(),
address_filter: None,
}
}
/// hyper defaults to 100 header fields per HTTP/1 response and fails the
/// whole request past that, which is what users hit in the field.
#[tokio::test]
async fn responses_with_more_than_100_headers_are_accepted() {
let url = serve_response_with_header_count(150).await;
let (client, _resolver) = options().build_client().unwrap();
let response = client
.inner()
.get(&url)
.send()
.await
.expect("request with 150 response headers should succeed");
assert_eq!(response.status(), 200);
assert_eq!(response.headers().len(), 150);
for i in 0..149 {
assert_eq!(
response.headers().get(format!("x-test-{i}")).unwrap(),
format!("value-{i}").as_str(),
);
}
}
/// Pins both sides of the boundary. The sizes are written out rather than
/// derived from `HTTP1_MAX_RESPONSE_HEADERS`, so retuning the limit trips
/// this test instead of silently moving with it.
#[tokio::test]
async fn the_limit_is_exactly_1024_header_fields() {
let (client, _resolver) = options().build_client().unwrap();
let at_limit = serve_response_with_header_count(1024).await;
let response = client
.inner()
.get(&at_limit)
.send()
.await
.expect("a response with 1024 header fields should succeed");
assert_eq!(response.headers().len(), 1024);
let over_limit = serve_response_with_header_count(1025).await;
assert!(client.inner().get(&over_limit).send().await.is_err());
}
}