Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit a9d7292

Browse files
0xMassiclaude
andcommitted
fix: correct HTTP header wire order + add H2 PRIORITY flag
Header order: reqwest pre-inserted `accept: */*` at position 0 in ClientBuilder::new(), pinning it ahead of sec-ch-ua headers. Fixed by replacing the header map instead of merging. Moved user-agent into each profile's default_headers at the correct Chrome/Firefox/Safari wire position. H2 PRIORITY: Real Chrome sends HEADERS frames with PRIORITY flag (weight=256, dep=0, exclusive). Added h2_headers_priority field to BrowserProfile and wired it through reqwest → hyper → h2. Before: accept[4] user-agent[5] sec-ch-ua[6] After: sec-ch-ua[4] user-agent[8] accept[9] (matches real Chrome) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e173a4c commit a9d7292

3 files changed

Lines changed: 24 additions & 8 deletions

File tree

webclaw-http/src/client.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,15 +261,15 @@ impl ClientBuilder {
261261

262262
let mut builder = reqwest::Client::builder()
263263
.rustls_config(tls_config)
264-
.user_agent(profile.user_agent)
265264
.timeout(self.timeout)
266265
.cookie_store(self.cookie_store)
267266
.http2_header_table_size(h2.header_table_size)
268267
.http2_enable_push(h2.enable_push)
269268
.http2_initial_stream_window_size(h2.initial_window_size)
270269
.http2_initial_connection_window_size(profile.h2_connection_window)
271270
.http2_settings_order(settings_order)
272-
.http2_headers_pseudo_order(pseudo_order);
271+
.http2_headers_pseudo_order(pseudo_order)
272+
.http2_headers_priority(profile.h2_headers_priority);
273273

274274
if h2.max_header_list_size > 0 {
275275
builder = builder.http2_max_header_list_size(h2.max_header_list_size);

webclaw-http/src/profiles.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ pub struct BrowserProfile {
4747
pub settings_order: &'static [H2Setting],
4848
/// HTTP/2 pseudo-header wire order.
4949
pub pseudo_order: &'static [PseudoHeader],
50+
/// HTTP/2 PRIORITY in HEADERS frame: (weight, stream_dep, exclusive).
51+
/// Chrome: Some((255, 0, true)) — weight 256, dep 0, exclusive.
52+
/// None = no PRIORITY flag (detectable as non-browser).
53+
pub h2_headers_priority: Option<(u8, u32, bool)>,
5054
}
5155

5256
impl BrowserProfile {
@@ -99,7 +103,7 @@ pub fn chrome() -> BrowserProfile {
99103
("sec-ch-ua-mobile", "?0"),
100104
("sec-ch-ua-platform", "\"Windows\""),
101105
("upgrade-insecure-requests", "1"),
102-
// user-agent is set separately via reqwest builder
106+
("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"),
103107
("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"),
104108
("sec-fetch-site", "none"),
105109
("sec-fetch-mode", "navigate"),
@@ -122,6 +126,8 @@ pub fn chrome() -> BrowserProfile {
122126
PseudoHeader::Scheme,
123127
PseudoHeader::Path,
124128
],
129+
// Chrome: weight=256 (255 on wire), dep=0, exclusive=true
130+
h2_headers_priority: Some((255, 0, true)),
125131
}
126132
}
127133

@@ -134,6 +140,7 @@ pub fn chrome_macos() -> BrowserProfile {
134140
("sec-ch-ua-mobile", "?0"),
135141
("sec-ch-ua-platform", "\"macOS\""),
136142
("upgrade-insecure-requests", "1"),
143+
("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"),
137144
("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"),
138145
("sec-fetch-site", "none"),
139146
("sec-fetch-mode", "navigate"),
@@ -162,7 +169,10 @@ pub fn firefox() -> BrowserProfile {
162169
},
163170
h2_connection_window: 12517377,
164171
default_headers: &[
165-
// user-agent set separately
172+
(
173+
"user-agent",
174+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0",
175+
),
166176
(
167177
"accept",
168178
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
@@ -190,6 +200,8 @@ pub fn firefox() -> BrowserProfile {
190200
PseudoHeader::Authority,
191201
PseudoHeader::Scheme,
192202
],
203+
// Firefox deprecated PRIORITY frames in HTTP/2
204+
h2_headers_priority: None,
193205
}
194206
}
195207

@@ -213,7 +225,7 @@ pub fn safari() -> BrowserProfile {
213225
("sec-fetch-dest", "document"),
214226
("accept-language", "en-GB,en;q=0.9"),
215227
("sec-fetch-mode", "navigate"),
216-
// user-agent set separately
228+
("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15"),
217229
("accept-encoding", "gzip, deflate, br"),
218230
("priority", "u=0, i"),
219231
],
@@ -231,6 +243,8 @@ pub fn safari() -> BrowserProfile {
231243
PseudoHeader::Authority,
232244
PseudoHeader::Scheme,
233245
],
246+
// Safari uses its own priority scheme
247+
h2_headers_priority: Some((255, 0, false)),
234248
}
235249
}
236250

@@ -244,6 +258,7 @@ pub fn edge() -> BrowserProfile {
244258
("sec-ch-ua-mobile", "?0"),
245259
("sec-ch-ua-platform", "\"Windows\""),
246260
("upgrade-insecure-requests", "1"),
261+
("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"),
247262
("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"),
248263
("sec-fetch-site", "none"),
249264
("sec-fetch-mode", "navigate"),

webclaw-reqwest/src/async_impl/client.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,9 +1207,10 @@ impl ClientBuilder {
12071207
/// # }
12081208
/// ```
12091209
pub fn default_headers(mut self, headers: HeaderMap) -> ClientBuilder {
1210-
for (key, value) in headers.iter() {
1211-
self.config.headers.insert(key, value.clone());
1212-
}
1210+
// Replace entirely to preserve caller's insertion order.
1211+
// The default `accept: */*` from ClientBuilder::new() would otherwise
1212+
// pin `accept` at position 0, breaking HTTP header order fingerprinting.
1213+
self.config.headers = headers;
12131214
self
12141215
}
12151216

0 commit comments

Comments
 (0)