Skip to content

Commit 08e93d6

Browse files
committed
Implement HTTP client configuration groups with reusable settings for transport, connection pool, TLS, protocol, and proxy options
- Added `config_groups.rs` to define grouped configuration options for the HTTP client. - Introduced `TransportConfigOptions`, `PoolConfigOptions`, `TlsConfigOptions`, `ProtocolConfigOptions`, and `ProxyConfigOptions` structs with default values and builder methods for customization. - Enhanced the `get_all` method in the cookie module to iterate over stored cookies more efficiently. - Updated hooks service to improve error handling and response processing in the client layer. - Added assertions in the delay service tests to ensure proper functionality.
1 parent f4c7870 commit 08e93d6

18 files changed

Lines changed: 1179 additions & 274 deletions

File tree

Cargo.toml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ members = ["crates/*", "bin/*"]
33
resolver = "3"
44

55
[workspace.package]
6-
version = "2.4.4"
6+
version = "2.4.5"
77
edition = "2024"
88
authors = ["Akagi201 <akagi201@gmail.com>"]
99
license = "Apache-2.0"
@@ -185,10 +185,10 @@ all = "warn"
185185

186186
[workspace.dependencies]
187187
# local crates
188-
fastwebsockets = { path = "crates/fastwebsockets", package = "hpx-fastwebsockets", version = "2.4.4" }
189-
hpx = { path = "crates/hpx", version = "2.4.4" }
190-
hpx-dl = { path = "crates/hpx-dl", version = "2.4.4" }
191-
hpx-yawc = { path = "crates/yawc", version = "2.4.4" }
188+
fastwebsockets = { path = "crates/fastwebsockets", package = "hpx-fastwebsockets", version = "2.4.5" }
189+
hpx = { path = "crates/hpx", version = "2.4.5" }
190+
hpx-dl = { path = "crates/hpx-dl", version = "2.4.5" }
191+
hpx-yawc = { path = "crates/yawc", version = "2.4.5" }
192192

193193
# external crates
194194
ahash = "0.8.12"
@@ -210,7 +210,7 @@ cucumber = "0.22.1"
210210
digest = "=0.10.7"
211211
encoding_rs = "0.8.35"
212212
eyre = "0.6.12"
213-
fastrand = "2.3.0"
213+
fastrand = "2.4.1"
214214
flate2 = "1.1.9"
215215
futures = "0.3.32"
216216
futures-channel = "0.3.32"
@@ -220,7 +220,7 @@ futures-util = "0.3.32"
220220
getrandom = "0.4.2"
221221
hex = "0.4.3"
222222
hickory-resolver = "0.25.2"
223-
hmac = "0.12.1"
223+
hmac = "0.13.0"
224224
home = "0.5.12"
225225
http = "1.4.0"
226226
http-body = "1.0.1"
@@ -233,7 +233,7 @@ hyper-util = "0.1.20"
233233
ipnet = "2.12.0"
234234
js-sys = "0.3.91"
235235
libc = "0.2.183"
236-
md-5 = "0.10.6"
236+
md-5 = "0.10.0"
237237
memchr = "2.8.0"
238238
mime = "0.3.17"
239239
mime_guess = "2.0.5"
@@ -253,14 +253,14 @@ rustls = "0.23.37"
253253
rustls-native-certs = "0.8.3"
254254
rustls-pemfile = "2.2.0"
255255
rustls-pki-types = "1.14.0"
256-
rustyline = "17.0.2"
256+
rustyline = "18.0.0"
257257
scc = "3.6.11"
258258
schnellru = "0.2.4"
259259
serde = "1.0.228"
260260
serde_json = "1.0.149"
261261
serde_urlencoded = "0.7.1"
262-
sha1 = "0.10.6"
263-
sha2 = "0.10.8"
262+
sha1 = "0.10.0"
263+
sha2 = "0.10.0"
264264
simd-json = "0.17.0"
265265
simdutf8 = "0.1.5"
266266
smallvec = "1.15.1"

README.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ An ergonomic all-in-one HTTP client for browser emulation with TLS, JA3/JA4, and
1515

1616
| Crate | Version | Description |
1717
|-------|---------|-------------|
18-
| [`hpx`](https://crates.io/crates/hpx) | 2.4.4 | High Performance HTTP Client |
19-
| [`hpx-emulation`](https://crates.io/crates/hpx-emulation) | 2.4.4 | Browser emulation profiles |
20-
| [`hpx-yawc`](https://crates.io/crates/hpx-yawc) | 2.4.4 | WebSocket library (RFC 6455 + compression) |
21-
| [`hpx-fastwebsockets`](https://crates.io/crates/hpx-fastwebsockets) | 2.4.4 | Fast minimal WebSocket implementation |
18+
| [`hpx`](https://crates.io/crates/hpx) | 2.4.5 | High Performance HTTP Client |
19+
| [`hpx-emulation`](https://crates.io/crates/hpx-emulation) | 2.4.5 | Browser emulation profiles |
20+
| [`hpx-yawc`](https://crates.io/crates/hpx-yawc) | 2.4.5 | WebSocket library (RFC 6455 + compression) |
21+
| [`hpx-fastwebsockets`](https://crates.io/crates/hpx-fastwebsockets) | 2.4.5 | Fast minimal WebSocket implementation |
2222

2323
## Features
2424

@@ -64,7 +64,7 @@ Add `hpx` to your `Cargo.toml`:
6464

6565
```toml
6666
[dependencies]
67-
hpx = "2.4.4"
67+
hpx = "2.4.5"
6868
```
6969

7070
The default features include **BoringSSL** TLS, **HTTP/1.1**, and **HTTP/2** support.
@@ -73,8 +73,8 @@ For browser emulation, add the utility crate:
7373

7474
```toml
7575
[dependencies]
76-
hpx = "2.4.4"
77-
hpx-emulation = "2.4.4"
76+
hpx = "2.4.5"
77+
hpx-emulation = "2.4.5"
7878
```
7979

8080
## Feature Flags
@@ -125,7 +125,7 @@ The `ws` feature is an alias for `ws-yawc` (the default WebSocket backend). To u
125125

126126
```toml
127127
[dependencies]
128-
hpx = { version = "2.4.4", features = ["ws-fastwebsockets"] }
128+
hpx = { version = "2.4.5", features = ["ws-fastwebsockets"] }
129129
```
130130

131131
When both `ws-yawc` and `ws-fastwebsockets` are enabled, fastwebsockets takes priority.
@@ -135,37 +135,37 @@ When both `ws-yawc` and `ws-fastwebsockets` are enabled, fastwebsockets takes pr
135135
**Minimal HTTP client:**
136136

137137
```toml
138-
hpx = "2.4.4" # default: boring + http1 + http2
138+
hpx = "2.4.5" # default: boring + http1 + http2
139139
```
140140

141141
**JSON API client:**
142142

143143
```toml
144-
hpx = { version = "2.4.4", features = ["json", "cookies", "gzip"] }
144+
hpx = { version = "2.4.5", features = ["json", "cookies", "gzip"] }
145145
```
146146

147147
**WebSocket client:**
148148

149149
```toml
150-
hpx = { version = "2.4.4", features = ["ws"] }
150+
hpx = { version = "2.4.5", features = ["ws"] }
151151
```
152152

153153
**High-performance trading:**
154154

155155
```toml
156-
hpx = { version = "2.4.4", features = ["simd-json", "hickory-dns", "zstd", "ws"] }
156+
hpx = { version = "2.4.5", features = ["simd-json", "hickory-dns", "zstd", "ws"] }
157157
```
158158

159159
**Pure Rust (no C dependencies):**
160160

161161
```toml
162-
hpx = { version = "2.4.4", default-features = false, features = ["rustls-tls", "http1", "http2"] }
162+
hpx = { version = "2.4.5", default-features = false, features = ["rustls-tls", "http1", "http2"] }
163163
```
164164

165165
**Full-featured:**
166166

167167
```toml
168-
hpx = { version = "2.4.4", features = [
168+
hpx = { version = "2.4.5", features = [
169169
"json", "form", "query", "multipart", "stream",
170170
"cookies", "charset",
171171
"gzip", "brotli", "zstd", "deflate",
@@ -206,7 +206,7 @@ BoringSSL is the default TLS backend, providing robust support for modern TLS fe
206206

207207
```toml
208208
[dependencies]
209-
hpx = "2.4.4"
209+
hpx = "2.4.5"
210210
```
211211

212212
### Rustls
@@ -217,7 +217,7 @@ A pure Rust TLS implementation. Useful for environments where C dependencies are
217217
218218
```toml
219219
[dependencies]
220-
hpx = { version = "2.4.4", default-features = false, features = ["rustls-tls", "http1", "http2"] }
220+
hpx = { version = "2.4.5", default-features = false, features = ["rustls-tls", "http1", "http2"] }
221221
```
222222

223223
## Usage Examples

crates/hpx/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ rustdoc-args = ["--cfg", "docsrs"]
2222
targets = ["x86_64-unknown-linux-gnu"]
2323

2424
[features]
25-
default = ["boring", "http1", "http2"]
25+
default = ["boring", "http1", "http2", "stream", "tracing"]
2626

2727
# Enable support for decoding text.
2828
charset = ["dep:encoding_rs", "dep:mime"]

crates/hpx/src/client.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ pub use self::{
2727
conn::HttpInfo,
2828
core::upgrade::Upgraded,
2929
emulation::{BrowserProfile, Emulation, EmulationBuilder, EmulationFactory},
30-
http::{Client, ClientBuilder},
30+
http::{
31+
Client, ClientBuilder, HttpVersionPreference, PoolConfigOptions, ProtocolConfigOptions,
32+
ProxyConfigOptions, TlsConfigOptions, TransportConfigOptions,
33+
},
3134
request::{Request, RequestBuilder},
3235
response::Response,
3336
};

crates/hpx/src/client/body.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#![allow(clippy::items_after_test_module)]
2+
13
use std::{
24
fmt,
35
pin::Pin,

crates/hpx/src/client/core/conn/http1.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,16 @@ impl Builder {
295295
}
296296

297297
let cd = proto::h1::dispatch::Client::new(rx);
298-
let proto = proto::h1::Dispatcher::new(cd, conn);
298+
let proto = proto::h1::Dispatcher::new_with_config(
299+
cd,
300+
conn,
301+
proto::h1::dispatch::PollConfig {
302+
max_iterations: self
303+
.opts
304+
.h1_max_poll_iterations
305+
.unwrap_or(proto::h1::dispatch::DEFAULT_MAX_POLL_ITERATIONS),
306+
},
307+
);
299308

300309
Ok((SendRequest { dispatch: tx }, Connection { inner: proto }))
301310
}

crates/hpx/src/client/core/http1.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ pub struct Http1Options {
3737

3838
/// Whether to allow obsolete multiline headers in HTTP/1 responses.
3939
pub allow_obsolete_multiline_headers_in_responses: bool,
40+
41+
/// Maximum number of dispatcher loop iterations per scheduler poll.
42+
pub h1_max_poll_iterations: Option<usize>,
4043
}
4144

4245
impl Http1OptionsBuilder {
@@ -161,6 +164,20 @@ impl Http1OptionsBuilder {
161164
self
162165
}
163166

167+
/// Set the maximum number of dispatcher iterations per scheduler poll.
168+
///
169+
/// The minimum value is 1.
170+
#[inline]
171+
pub fn max_poll_iterations(mut self, max_iterations: usize) -> Self {
172+
assert!(
173+
max_iterations > 0,
174+
"max_poll_iterations must be greater than zero"
175+
);
176+
177+
self.opts.h1_max_poll_iterations = Some(max_iterations);
178+
self
179+
}
180+
164181
/// Build the [`Http1Options`] instance.
165182
#[inline]
166183
pub fn build(self) -> Http1Options {

crates/hpx/src/client/core/proto/h1/dispatch.rs

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,30 @@ use crate::client::core::{
2020
upgrade::OnUpgrade,
2121
};
2222

23+
/// Default number of HTTP/1 dispatcher iterations processed in a single poll.
24+
pub(crate) const DEFAULT_MAX_POLL_ITERATIONS: usize = 16;
25+
26+
/// Configuration for the HTTP/1 dispatcher poll loop.
27+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28+
pub(crate) struct PollConfig {
29+
pub(crate) max_iterations: usize,
30+
}
31+
32+
impl Default for PollConfig {
33+
fn default() -> Self {
34+
Self {
35+
max_iterations: DEFAULT_MAX_POLL_ITERATIONS,
36+
}
37+
}
38+
}
39+
2340
pub(crate) struct Dispatcher<D, Bs: Body, I, T> {
2441
conn: Conn<I, Bs::Data, T>,
2542
dispatch: D,
2643
body_tx: Option<body::Sender>,
2744
body_rx: Pin<Box<Option<Bs>>>,
2845
is_closing: bool,
46+
poll_config: PollConfig,
2947
}
3048

3149
pub(crate) trait Dispatch {
@@ -69,12 +87,21 @@ where
6987
Bs::Error: Into<BoxError>,
7088
{
7189
pub(crate) fn new(dispatch: D, conn: Conn<I, Bs::Data, T>) -> Self {
90+
Self::new_with_config(dispatch, conn, PollConfig::default())
91+
}
92+
93+
pub(crate) fn new_with_config(
94+
dispatch: D,
95+
conn: Conn<I, Bs::Data, T>,
96+
poll_config: PollConfig,
97+
) -> Self {
7298
Dispatcher {
7399
conn,
74100
dispatch,
75101
body_tx: None,
76102
body_rx: Box::pin(None),
77103
is_closing: false,
104+
poll_config,
78105
}
79106
}
80107

@@ -129,9 +156,7 @@ where
129156
// Limit the looping on this connection, in case it is ready far too
130157
// often, so that other futures don't starve.
131158
//
132-
// 16 was chosen arbitrarily, as that is number of pipelined requests
133-
// benchmarks often use. Perhaps it should be a config option instead.
134-
for _ in 0..16 {
159+
for _ in 0..self.poll_config.max_iterations {
135160
let _ = self.poll_read(cx)?;
136161
let _ = self.poll_write(cx)?;
137162
let _ = self.poll_flush(cx)?;
@@ -148,6 +173,15 @@ where
148173
// break;
149174
return Poll::Ready(Ok(()));
150175
}
176+
177+
let mut consume_budget = std::pin::pin!(tokio::task::consume_budget());
178+
if consume_budget.as_mut().poll(cx).is_pending() {
179+
trace!(
180+
"poll_loop yielding due to cooperative budget (self = {:p})",
181+
self
182+
);
183+
return Poll::Pending;
184+
}
151185
}
152186

153187
trace!("poll_loop yielding (self = {:p})", self);
@@ -681,4 +715,18 @@ mod tests {
681715
// If it is, it will trigger an assertion.
682716
assert!(dispatcher.poll().is_pending());
683717
}
718+
719+
#[test]
720+
fn dispatcher_uses_custom_poll_config() {
721+
let io = tokio_test::io::Builder::new().build();
722+
let (_tx, rx) = dispatch::channel();
723+
let conn = Conn::<_, bytes::Bytes, ClientTransaction>::new(io);
724+
let dispatcher = Dispatcher::new_with_config(
725+
Client::<IncomingBody>::new(rx),
726+
conn,
727+
PollConfig { max_iterations: 7 },
728+
);
729+
730+
assert_eq!(dispatcher.poll_config.max_iterations, 7);
731+
}
684732
}

crates/hpx/src/client/core/proto/h1/io.rs

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use std::{
55
task::{Context, Poll, ready},
66
};
77

8-
use bytes::{Buf, BufMut, Bytes, BytesMut};
9-
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
8+
use bytes::{Buf, Bytes, BytesMut};
9+
use tokio::io::{AsyncRead, AsyncWrite};
1010

1111
use super::{Http1Transaction, ParseContext, ParsedMessage};
1212
use crate::client::core::{self, Error, common::buf::BufList};
@@ -204,30 +204,22 @@ where
204204
self.read_buf.reserve(next);
205205
}
206206

207-
// SAFETY: ReadBuf and poll_read promise not to set any uninitialized
208-
// bytes onto `dst`.
209-
#[allow(unsafe_code)]
210-
let dst = unsafe { self.read_buf.chunk_mut().as_uninit_slice_mut() };
211-
let mut buf = ReadBuf::uninit(dst);
212-
match Pin::new(&mut self.io).poll_read(cx, &mut buf) {
213-
Poll::Ready(Ok(_)) => {
214-
let n = buf.filled().len();
207+
// Use tokio_util::io::poll_read_buf for safe buffer handling
208+
// instead of manual unsafe operations
209+
match tokio_util::io::poll_read_buf(Pin::new(&mut self.io), cx, &mut self.read_buf) {
210+
Poll::Ready(Ok(n)) => {
215211
trace!("received {} bytes", n);
216-
#[allow(unsafe_code)]
217-
unsafe {
218-
// Safety: we just read that many bytes into the
219-
// uninitialized part of the buffer, so this is okay.
220-
// @tokio pls give me back `poll_read_buf` thanks
221-
self.read_buf.advance_mut(n);
222-
}
223212
self.read_buf_strategy.record(n);
224213
Poll::Ready(Ok(n))
225214
}
215+
Poll::Ready(Err(e)) => {
216+
debug!("read error: {}", e);
217+
Poll::Ready(Err(e))
218+
}
226219
Poll::Pending => {
227220
self.read_blocked = true;
228221
Poll::Pending
229222
}
230-
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
231223
}
232224
}
233225

0 commit comments

Comments
 (0)