wreq 1.2.4 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +3 -7
- data/Cargo.toml +6 -1
- data/crates/wreq-util/.github/FUNDING.yml +15 -0
- data/crates/wreq-util/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- data/crates/wreq-util/.github/ISSUE_TEMPLATE/custom.md +10 -0
- data/crates/wreq-util/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/crates/wreq-util/.github/dependabot.yml +22 -0
- data/crates/wreq-util/.github/workflows/ci.yml +82 -0
- data/crates/wreq-util/.github/workflows/release-plz.yml +52 -0
- data/crates/wreq-util/.gitignore +24 -0
- data/crates/wreq-util/CHANGELOG.md +74 -0
- data/crates/wreq-util/Cargo.toml +94 -0
- data/crates/wreq-util/LICENSE +201 -0
- data/crates/wreq-util/README.md +62 -0
- data/crates/wreq-util/examples/emulate.rs +39 -0
- data/crates/wreq-util/examples/tower_delay.rs +191 -0
- data/crates/wreq-util/release-plz.toml +2 -0
- data/crates/wreq-util/rustfmt.toml +5 -0
- data/crates/wreq-util/src/emulate/compress.rs +85 -0
- data/crates/wreq-util/src/emulate/macros.rs +370 -0
- data/crates/wreq-util/src/emulate/profile/chrome/header.rs +59 -0
- data/crates/wreq-util/src/emulate/profile/chrome/http2.rs +65 -0
- data/crates/wreq-util/src/emulate/profile/chrome/tls.rs +153 -0
- data/crates/wreq-util/src/emulate/profile/chrome.rs +2028 -0
- data/crates/wreq-util/src/emulate/profile/firefox/header.rs +21 -0
- data/crates/wreq-util/src/emulate/profile/firefox/http2.rs +113 -0
- data/crates/wreq-util/src/emulate/profile/firefox/tls.rs +253 -0
- data/crates/wreq-util/src/emulate/profile/firefox.rs +518 -0
- data/crates/wreq-util/src/emulate/profile/okhttp.rs +241 -0
- data/crates/wreq-util/src/emulate/profile/opera/header.rs +30 -0
- data/crates/wreq-util/src/emulate/profile/opera/http2.rs +50 -0
- data/crates/wreq-util/src/emulate/profile/opera/tls.rs +97 -0
- data/crates/wreq-util/src/emulate/profile/opera.rs +299 -0
- data/crates/wreq-util/src/emulate/profile/safari/header.rs +59 -0
- data/crates/wreq-util/src/emulate/profile/safari/http2.rs +116 -0
- data/crates/wreq-util/src/emulate/profile/safari/tls.rs +177 -0
- data/crates/wreq-util/src/emulate/profile/safari.rs +222 -0
- data/crates/wreq-util/src/emulate/profile.rs +47 -0
- data/crates/wreq-util/src/emulate.rs +369 -0
- data/crates/wreq-util/src/lib.rs +14 -0
- data/crates/wreq-util/src/rand.rs +24 -0
- data/crates/wreq-util/src/tower/delay/future.rs +43 -0
- data/crates/wreq-util/src/tower/delay/layer.rs +201 -0
- data/crates/wreq-util/src/tower/delay/service.rs +190 -0
- data/crates/wreq-util/src/tower/delay.rs +98 -0
- data/crates/wreq-util/src/tower.rs +7 -0
- data/crates/wreq-util/tests/client.rs +51 -0
- data/crates/wreq-util/tests/emulate_chrome.rs +270 -0
- data/crates/wreq-util/tests/emulate_firefox.rs +92 -0
- data/crates/wreq-util/tests/emulate_okhttp.rs +46 -0
- data/crates/wreq-util/tests/emulate_opera.rs +113 -0
- data/crates/wreq-util/tests/emulate_safari.rs +151 -0
- data/crates/wreq-util/tests/support/mod.rs +55 -0
- data/crates/wreq-util/tests/support/server.rs +232 -0
- data/examples/emulate_request.rb +1 -1
- data/lib/wreq.rb +20 -18
- data/lib/wreq_ruby/body.rb +3 -0
- data/lib/wreq_ruby/client.rb +18 -18
- data/lib/wreq_ruby/cookie.rb +2 -2
- data/lib/wreq_ruby/emulate.rb +3 -1
- data/lib/wreq_ruby/header.rb +205 -114
- data/lib/wreq_ruby/response.rb +28 -3
- data/src/emulate.rs +1 -0
- data/src/error.rs +2 -6
- data/src/header/helper.rs +112 -0
- data/src/header.rs +198 -133
- data/test/header_test.rb +228 -192
- data/test/stream_test.rb +36 -0
- metadata +54 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
pin::Pin,
|
|
3
|
+
task::{Context, Poll, ready},
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
use pin_project_lite::pin_project;
|
|
7
|
+
use tokio::time::Sleep;
|
|
8
|
+
use tower::BoxError;
|
|
9
|
+
|
|
10
|
+
pin_project! {
|
|
11
|
+
/// Response future for [`Delay`].
|
|
12
|
+
///
|
|
13
|
+
/// [`Delay`]: super::Delay
|
|
14
|
+
#[derive(Debug)]
|
|
15
|
+
pub struct ResponseFuture<S> {
|
|
16
|
+
#[pin]
|
|
17
|
+
response: S,
|
|
18
|
+
#[pin]
|
|
19
|
+
sleep: Sleep,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl<S> ResponseFuture<S> {
|
|
24
|
+
// Create a new [`ResponseFuture`]
|
|
25
|
+
#[inline]
|
|
26
|
+
pub(crate) fn new(response: S, sleep: Sleep) -> Self {
|
|
27
|
+
ResponseFuture { response, sleep }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl<F, S, E> Future for ResponseFuture<F>
|
|
32
|
+
where
|
|
33
|
+
F: Future<Output = Result<S, E>>,
|
|
34
|
+
E: Into<BoxError>,
|
|
35
|
+
{
|
|
36
|
+
type Output = Result<S, BoxError>;
|
|
37
|
+
|
|
38
|
+
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
|
39
|
+
let this = self.project();
|
|
40
|
+
ready!(this.sleep.poll(cx));
|
|
41
|
+
this.response.poll(cx).map_err(Into::into)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
use std::time::Duration;
|
|
2
|
+
|
|
3
|
+
use tower::Layer;
|
|
4
|
+
|
|
5
|
+
use super::service::{Delay, DelayWith, JitterDelay, JitterDelayWith};
|
|
6
|
+
|
|
7
|
+
/// A Tower [`Layer`] that introduces a fixed delay before each request.
|
|
8
|
+
#[derive(Clone, Debug)]
|
|
9
|
+
pub struct DelayLayer {
|
|
10
|
+
delay: Duration,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/// Conditional delay [`Layer`], applies delay based on a predicate.
|
|
14
|
+
///
|
|
15
|
+
/// Created via [`DelayLayer::when`].
|
|
16
|
+
#[derive(Clone, Debug)]
|
|
17
|
+
pub struct DelayLayerWith<P> {
|
|
18
|
+
delay: Duration,
|
|
19
|
+
predicate: P,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// A Tower [`Layer`] that introduces a jittered delay before each request.
|
|
23
|
+
///
|
|
24
|
+
/// The actual delay for each request will be randomly chosen within the range
|
|
25
|
+
/// `[base - base*pct, base + base*pct]`.
|
|
26
|
+
///
|
|
27
|
+
/// # Example
|
|
28
|
+
///
|
|
29
|
+
/// ```no_run
|
|
30
|
+
/// use std::time::Duration;
|
|
31
|
+
/// use wreq::Client;
|
|
32
|
+
/// use wreq_util::middleware::delay::JitterDelayLayer;
|
|
33
|
+
///
|
|
34
|
+
/// // Creates delays in range [0.8s, 1.2s] (1s ± 20%)
|
|
35
|
+
/// let client = Client::builder()
|
|
36
|
+
/// .layer(JitterDelayLayer::new(Duration::from_secs(1), 0.2))
|
|
37
|
+
/// .build()?;
|
|
38
|
+
/// # Ok::<(), wreq::Error>(())
|
|
39
|
+
/// ```
|
|
40
|
+
#[derive(Clone, Debug)]
|
|
41
|
+
pub struct JitterDelayLayer {
|
|
42
|
+
base: Duration,
|
|
43
|
+
pct: f64,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Conditional jitter delay [`Layer`], applies delay based on a predicate.
|
|
47
|
+
///
|
|
48
|
+
/// Created via [`JitterDelayLayer::when`].
|
|
49
|
+
#[derive(Clone, Debug)]
|
|
50
|
+
pub struct JitterDelayLayerWith<P> {
|
|
51
|
+
base: Duration,
|
|
52
|
+
pct: f64,
|
|
53
|
+
predicate: P,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ===== impl DelayLayer =====
|
|
57
|
+
|
|
58
|
+
impl DelayLayer {
|
|
59
|
+
/// Create a new [`DelayLayer`] with the given delay duration.
|
|
60
|
+
#[inline]
|
|
61
|
+
pub const fn new(delay: Duration) -> Self {
|
|
62
|
+
DelayLayer { delay }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Apply delay only to requests that satisfy a predicate.
|
|
66
|
+
///
|
|
67
|
+
/// Requests that don't match the predicate will pass through without delay.
|
|
68
|
+
///
|
|
69
|
+
/// # Example
|
|
70
|
+
///
|
|
71
|
+
/// ```ignore
|
|
72
|
+
/// use std::time::Duration;
|
|
73
|
+
/// use http::Request;
|
|
74
|
+
/// use wreq_util::middleware::delay::DelayLayer;
|
|
75
|
+
///
|
|
76
|
+
/// // Only delay POST requests
|
|
77
|
+
/// let layer = DelayLayer::new(Duration::from_secs(1))
|
|
78
|
+
/// .when(|req: &Request<_>| req.method() == http::Method::POST);
|
|
79
|
+
/// ```
|
|
80
|
+
pub fn when<P, Req>(self, predicate: P) -> DelayLayerWith<P>
|
|
81
|
+
where
|
|
82
|
+
P: Fn(&Req) -> bool + Clone,
|
|
83
|
+
{
|
|
84
|
+
DelayLayerWith::new(self.delay, predicate)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
impl<S> Layer<S> for DelayLayer {
|
|
89
|
+
type Service = Delay<S>;
|
|
90
|
+
|
|
91
|
+
#[inline]
|
|
92
|
+
fn layer(&self, service: S) -> Self::Service {
|
|
93
|
+
Delay::new(service, self.delay)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ===== impl DelayLayerWith =====
|
|
98
|
+
|
|
99
|
+
impl<P> DelayLayerWith<P> {
|
|
100
|
+
/// Creates a new [`DelayLayerWith`].
|
|
101
|
+
#[inline]
|
|
102
|
+
pub fn new(delay: Duration, predicate: P) -> Self {
|
|
103
|
+
Self { delay, predicate }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
impl<P, S> Layer<S> for DelayLayerWith<P>
|
|
108
|
+
where
|
|
109
|
+
P: Clone,
|
|
110
|
+
{
|
|
111
|
+
type Service = DelayWith<S, P>;
|
|
112
|
+
|
|
113
|
+
#[inline]
|
|
114
|
+
fn layer(&self, inner: S) -> Self::Service {
|
|
115
|
+
DelayWith::new(inner, self.delay, self.predicate.clone())
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ===== impl JitterDelayLayer =====
|
|
120
|
+
|
|
121
|
+
impl JitterDelayLayer {
|
|
122
|
+
/// Create a new [`JitterDelayLayer`] with the given base delay and jitter percentage.
|
|
123
|
+
///
|
|
124
|
+
/// # Arguments
|
|
125
|
+
/// * `base` - The base delay duration
|
|
126
|
+
/// * `pct` - The jitter percentage (0.0 to 1.0), representing ±pct deviation
|
|
127
|
+
///
|
|
128
|
+
/// # Example
|
|
129
|
+
///
|
|
130
|
+
/// ```
|
|
131
|
+
/// use std::time::Duration;
|
|
132
|
+
/// use wreq_util::middleware::delay::JitterDelayLayer;
|
|
133
|
+
///
|
|
134
|
+
/// // Creates delays in range [800ms, 1200ms]
|
|
135
|
+
/// let layer = JitterDelayLayer::new(Duration::from_secs(1), 0.2);
|
|
136
|
+
/// ```
|
|
137
|
+
#[inline]
|
|
138
|
+
pub fn new(base: Duration, pct: f64) -> Self {
|
|
139
|
+
Self {
|
|
140
|
+
base,
|
|
141
|
+
pct: pct.clamp(0.0, 1.0),
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Apply jitter delay only to requests that satisfy a predicate.
|
|
146
|
+
///
|
|
147
|
+
/// Requests that don't match the predicate will pass through without delay.
|
|
148
|
+
///
|
|
149
|
+
/// # Example
|
|
150
|
+
///
|
|
151
|
+
/// ```ignore
|
|
152
|
+
/// use std::time::Duration;
|
|
153
|
+
/// use http::Request;
|
|
154
|
+
/// use wreq_util::middleware::delay::JitterDelayLayer;
|
|
155
|
+
///
|
|
156
|
+
/// // Only delay requests to paths starting with "/slow"
|
|
157
|
+
/// let layer = JitterDelayLayer::new(Duration::from_secs(1), 0.2)
|
|
158
|
+
/// .when(|req: &Request<_>| req.uri().path().starts_with("/slow"));
|
|
159
|
+
/// ```
|
|
160
|
+
pub fn when<P, Req>(self, predicate: P) -> JitterDelayLayerWith<P>
|
|
161
|
+
where
|
|
162
|
+
P: Fn(&Req) -> bool + Clone,
|
|
163
|
+
{
|
|
164
|
+
JitterDelayLayerWith::new(self.base, self.pct, predicate)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
impl<S> Layer<S> for JitterDelayLayer {
|
|
169
|
+
type Service = JitterDelay<S>;
|
|
170
|
+
|
|
171
|
+
#[inline]
|
|
172
|
+
fn layer(&self, inner: S) -> Self::Service {
|
|
173
|
+
JitterDelay::new(inner, self.base, self.pct)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ===== impl JitterDelayLayerWith =====
|
|
178
|
+
|
|
179
|
+
impl<P> JitterDelayLayerWith<P> {
|
|
180
|
+
/// Creates a new [`JitterDelayLayerWith`].
|
|
181
|
+
#[inline]
|
|
182
|
+
pub fn new(base: Duration, pct: f64, predicate: P) -> Self {
|
|
183
|
+
Self {
|
|
184
|
+
base,
|
|
185
|
+
pct: pct.clamp(0.0, 1.0),
|
|
186
|
+
predicate,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
impl<P, S> Layer<S> for JitterDelayLayerWith<P>
|
|
192
|
+
where
|
|
193
|
+
P: Clone,
|
|
194
|
+
{
|
|
195
|
+
type Service = JitterDelayWith<S, P>;
|
|
196
|
+
|
|
197
|
+
#[inline]
|
|
198
|
+
fn layer(&self, inner: S) -> Self::Service {
|
|
199
|
+
JitterDelayWith::new(inner, self.base, self.pct, self.predicate.clone())
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
task::{Context, Poll},
|
|
3
|
+
time::Duration,
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
use tower::{BoxError, Service};
|
|
7
|
+
|
|
8
|
+
use super::{future::ResponseFuture, jittered_duration};
|
|
9
|
+
|
|
10
|
+
/// A Tower [`Service`] that introduces a fixed delay before each request.
|
|
11
|
+
#[derive(Debug, Clone)]
|
|
12
|
+
pub struct Delay<S> {
|
|
13
|
+
inner: S,
|
|
14
|
+
delay: Duration,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// A Tower [`Service`] that conditionally applies fixed delay based on a predicate.
|
|
18
|
+
///
|
|
19
|
+
/// Requests that match the predicate will have the delay applied;
|
|
20
|
+
/// other requests pass through immediately.
|
|
21
|
+
#[derive(Clone, Debug)]
|
|
22
|
+
pub struct DelayWith<S, P> {
|
|
23
|
+
inner: Delay<S>,
|
|
24
|
+
predicate: P,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// A Tower [`Service`] that applies jittered delay to requests.
|
|
28
|
+
///
|
|
29
|
+
/// This service wraps an inner service and introduces a random delay
|
|
30
|
+
/// (within a configured range) before each request.
|
|
31
|
+
#[derive(Clone, Debug)]
|
|
32
|
+
pub struct JitterDelay<S> {
|
|
33
|
+
inner: S,
|
|
34
|
+
base: Duration,
|
|
35
|
+
pct: f64,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// A Tower [`Service`] that conditionally applies jittered delay based on a predicate.
|
|
39
|
+
///
|
|
40
|
+
/// Requests that match the predicate will have a jittered delay applied;
|
|
41
|
+
/// other requests pass through immediately.
|
|
42
|
+
#[derive(Clone, Debug)]
|
|
43
|
+
pub struct JitterDelayWith<S, P> {
|
|
44
|
+
inner: JitterDelay<S>,
|
|
45
|
+
predicate: P,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ===== impl Delay =====
|
|
49
|
+
|
|
50
|
+
impl<S> Delay<S> {
|
|
51
|
+
/// Create a new [`Delay`] service wrapping the given inner service
|
|
52
|
+
#[inline]
|
|
53
|
+
pub fn new(inner: S, delay: Duration) -> Self {
|
|
54
|
+
Delay { inner, delay }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
impl<S, Request> Service<Request> for Delay<S>
|
|
59
|
+
where
|
|
60
|
+
S: Service<Request>,
|
|
61
|
+
S::Error: Into<BoxError>,
|
|
62
|
+
{
|
|
63
|
+
type Response = S::Response;
|
|
64
|
+
type Error = BoxError;
|
|
65
|
+
type Future = ResponseFuture<S::Future>;
|
|
66
|
+
|
|
67
|
+
#[inline]
|
|
68
|
+
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
|
69
|
+
self.inner.poll_ready(cx).map_err(Into::into)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn call(&mut self, req: Request) -> Self::Future {
|
|
73
|
+
let response = self.inner.call(req);
|
|
74
|
+
let sleep = tokio::time::sleep(self.delay);
|
|
75
|
+
ResponseFuture::new(response, sleep)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ===== impl DelayWith =====
|
|
80
|
+
|
|
81
|
+
impl<S, P> DelayWith<S, P> {
|
|
82
|
+
/// Creates a new [`DelayWith`].
|
|
83
|
+
#[inline]
|
|
84
|
+
pub fn new(inner: S, delay: Duration, predicate: P) -> Self {
|
|
85
|
+
Self {
|
|
86
|
+
inner: Delay::new(inner, delay),
|
|
87
|
+
predicate,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
impl<S, Req, P> Service<Req> for DelayWith<S, P>
|
|
93
|
+
where
|
|
94
|
+
S: Service<Req>,
|
|
95
|
+
S::Error: Into<BoxError>,
|
|
96
|
+
P: Fn(&Req) -> bool,
|
|
97
|
+
{
|
|
98
|
+
type Response = S::Response;
|
|
99
|
+
type Error = BoxError;
|
|
100
|
+
type Future = ResponseFuture<S::Future>;
|
|
101
|
+
|
|
102
|
+
#[inline]
|
|
103
|
+
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
|
104
|
+
self.inner.poll_ready(cx).map_err(Into::into)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fn call(&mut self, req: Req) -> Self::Future {
|
|
108
|
+
if !(self.predicate)(&req) {
|
|
109
|
+
self.inner.delay = Duration::ZERO;
|
|
110
|
+
}
|
|
111
|
+
self.inner.call(req)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ===== impl JitterDelay =====
|
|
116
|
+
|
|
117
|
+
impl<S> JitterDelay<S> {
|
|
118
|
+
/// Creates a new [`JitterDelay`].
|
|
119
|
+
#[inline]
|
|
120
|
+
pub fn new(inner: S, base: Duration, pct: f64) -> Self {
|
|
121
|
+
Self {
|
|
122
|
+
inner,
|
|
123
|
+
base,
|
|
124
|
+
pct: pct.clamp(0.0, 1.0),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
impl<S, Req> Service<Req> for JitterDelay<S>
|
|
130
|
+
where
|
|
131
|
+
S: Service<Req>,
|
|
132
|
+
S::Error: Into<BoxError>,
|
|
133
|
+
{
|
|
134
|
+
type Response = S::Response;
|
|
135
|
+
type Error = BoxError;
|
|
136
|
+
type Future = ResponseFuture<S::Future>;
|
|
137
|
+
|
|
138
|
+
#[inline]
|
|
139
|
+
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
|
140
|
+
self.inner.poll_ready(cx).map_err(Into::into)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn call(&mut self, req: Req) -> Self::Future {
|
|
144
|
+
let delay = jittered_duration(self.base, self.pct);
|
|
145
|
+
let sleep = tokio::time::sleep(delay);
|
|
146
|
+
let fut = self.inner.call(req);
|
|
147
|
+
ResponseFuture::new(fut, sleep)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ===== impl JitterDelayWith =====
|
|
152
|
+
|
|
153
|
+
impl<S, P> JitterDelayWith<S, P> {
|
|
154
|
+
/// Creates a new [`JitterDelayWith`].
|
|
155
|
+
#[inline]
|
|
156
|
+
pub fn new(inner: S, base: Duration, pct: f64, predicate: P) -> Self {
|
|
157
|
+
Self {
|
|
158
|
+
inner: JitterDelay::new(inner, base, pct),
|
|
159
|
+
predicate,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
impl<S, Req, P> Service<Req> for JitterDelayWith<S, P>
|
|
165
|
+
where
|
|
166
|
+
S: Service<Req>,
|
|
167
|
+
S::Error: Into<BoxError>,
|
|
168
|
+
P: Fn(&Req) -> bool,
|
|
169
|
+
{
|
|
170
|
+
type Response = S::Response;
|
|
171
|
+
type Error = BoxError;
|
|
172
|
+
type Future = ResponseFuture<S::Future>;
|
|
173
|
+
|
|
174
|
+
#[inline]
|
|
175
|
+
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
|
176
|
+
self.inner.poll_ready(cx).map_err(Into::into)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn call(&mut self, req: Req) -> Self::Future {
|
|
180
|
+
let delay = if (self.predicate)(&req) {
|
|
181
|
+
jittered_duration(self.inner.base, self.inner.pct)
|
|
182
|
+
} else {
|
|
183
|
+
Duration::ZERO
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
let sleep = tokio::time::sleep(delay);
|
|
187
|
+
let fut = self.inner.inner.call(req);
|
|
188
|
+
ResponseFuture::new(fut, sleep)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//! Request delay middleware.
|
|
2
|
+
//!
|
|
3
|
+
//! Adds configurable delays before HTTP requests — useful for rate limiting,
|
|
4
|
+
//! testing under slow network conditions, or just being polite to APIs.
|
|
5
|
+
//!
|
|
6
|
+
//! # Quick Start
|
|
7
|
+
//!
|
|
8
|
+
//! Fixed 1-second delay:
|
|
9
|
+
//!
|
|
10
|
+
//! ```no_run
|
|
11
|
+
//! use std::time::Duration;
|
|
12
|
+
//! use wreq::Client;
|
|
13
|
+
//! use wreq_util::middleware::delay::DelayLayer;
|
|
14
|
+
//!
|
|
15
|
+
//! let client = Client::builder()
|
|
16
|
+
//! .layer(DelayLayer::new(Duration::from_secs(1)))
|
|
17
|
+
//! .build()?;
|
|
18
|
+
//! # Ok::<(), wreq::Error>(())
|
|
19
|
+
//! ```
|
|
20
|
+
//!
|
|
21
|
+
//! Random jitter (0.8s ~ 1.2s):
|
|
22
|
+
//!
|
|
23
|
+
//! ```no_run
|
|
24
|
+
//! use std::time::Duration;
|
|
25
|
+
//! use wreq::Client;
|
|
26
|
+
//! use wreq_util::middleware::delay::JitterDelayLayer;
|
|
27
|
+
//!
|
|
28
|
+
//! let client = Client::builder()
|
|
29
|
+
//! .layer(JitterDelayLayer::new(Duration::from_secs(1), 0.2))
|
|
30
|
+
//! .build()?;
|
|
31
|
+
//! # Ok::<(), wreq::Error>(())
|
|
32
|
+
//! ```
|
|
33
|
+
//!
|
|
34
|
+
//! # Conditional Delays
|
|
35
|
+
//!
|
|
36
|
+
//! Use `.when()` to apply delays only to matching requests:
|
|
37
|
+
//!
|
|
38
|
+
//! ```ignore
|
|
39
|
+
//! // Only delay POST requests
|
|
40
|
+
//! DelayLayer::new(Duration::from_secs(1))
|
|
41
|
+
//! .when(|req: &http::Request<_>| req.method() == http::Method::POST)
|
|
42
|
+
//!
|
|
43
|
+
//! // Jitter on specific paths
|
|
44
|
+
//! JitterDelayLayer::new(Duration::from_millis(500), 0.3)
|
|
45
|
+
//! .when(|req: &http::Request<_>| req.uri().path().starts_with("/api"))
|
|
46
|
+
//! ```
|
|
47
|
+
//!
|
|
48
|
+
//! # Notes
|
|
49
|
+
//!
|
|
50
|
+
//! - Delays are async and won't block the runtime
|
|
51
|
+
//! - Not a substitute for proper rate limiters — servers can still see timing patterns
|
|
52
|
+
//! - Keep delays short in hot paths
|
|
53
|
+
|
|
54
|
+
mod future;
|
|
55
|
+
mod layer;
|
|
56
|
+
mod service;
|
|
57
|
+
|
|
58
|
+
use std::time::Duration;
|
|
59
|
+
|
|
60
|
+
pub use self::{
|
|
61
|
+
future::ResponseFuture,
|
|
62
|
+
layer::{DelayLayer, DelayLayerWith, JitterDelayLayer, JitterDelayLayerWith},
|
|
63
|
+
service::{Delay, DelayWith, JitterDelay, JitterDelayWith},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/// Compute a randomized duration in `[base * (1 - pct), base * (1 + pct)]`.
|
|
67
|
+
fn jittered_duration(base: Duration, pct: f64) -> Duration {
|
|
68
|
+
let jitter = base.mul_f64(pct);
|
|
69
|
+
let low = base.saturating_sub(jitter);
|
|
70
|
+
let high = base.saturating_add(jitter);
|
|
71
|
+
|
|
72
|
+
if low >= high {
|
|
73
|
+
return base;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Generate pseudo-random value using multiple entropy sources
|
|
77
|
+
let time_entropy = {
|
|
78
|
+
use std::time::SystemTime;
|
|
79
|
+
SystemTime::now()
|
|
80
|
+
.duration_since(SystemTime::UNIX_EPOCH)
|
|
81
|
+
.map(|d| d.as_nanos() as u64)
|
|
82
|
+
.unwrap_or(0)
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Use stack address as additional entropy source
|
|
86
|
+
let addr_entropy = (&time_entropy as *const u64 as u64).wrapping_mul(0x517cc1b727220a95);
|
|
87
|
+
|
|
88
|
+
// Mix entropy sources with a simple but effective hash
|
|
89
|
+
let mixed = time_entropy
|
|
90
|
+
.wrapping_add(addr_entropy)
|
|
91
|
+
.wrapping_mul(0x9e3779b97f4a7c15);
|
|
92
|
+
|
|
93
|
+
// Convert to fraction in [0, 1)
|
|
94
|
+
let frac = (mixed as f64) / (u64::MAX as f64);
|
|
95
|
+
|
|
96
|
+
let span = (high - low).as_secs_f64();
|
|
97
|
+
low + Duration::from_secs_f64(span * frac)
|
|
98
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#![cfg(not(target_arch = "wasm32"))]
|
|
2
|
+
mod support;
|
|
3
|
+
|
|
4
|
+
use support::server;
|
|
5
|
+
use wreq::Client;
|
|
6
|
+
use wreq_util::{Emulation, Platform};
|
|
7
|
+
|
|
8
|
+
#[tokio::test]
|
|
9
|
+
async fn test_client_emulation_device() {
|
|
10
|
+
let server = server::http(move |req| async move {
|
|
11
|
+
for (name, value) in req.headers() {
|
|
12
|
+
if name == "user-agent" {
|
|
13
|
+
assert_eq!(
|
|
14
|
+
value,
|
|
15
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
if name == "sec-ch-ua" {
|
|
19
|
+
assert_eq!(
|
|
20
|
+
value,
|
|
21
|
+
r#""Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133""#
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
if name == "sec-ch-ua-mobile" {
|
|
25
|
+
assert_eq!(value, "?0");
|
|
26
|
+
}
|
|
27
|
+
if name == "sec-ch-ua-platform" {
|
|
28
|
+
assert_eq!(value, "\"Linux\"");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
http::Response::default()
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let url = format!("http://{}/ua", server.addr());
|
|
35
|
+
let res = Client::builder()
|
|
36
|
+
.emulation(
|
|
37
|
+
Emulation::builder()
|
|
38
|
+
.profile(Emulation::Chrome133)
|
|
39
|
+
.platform(Platform::Linux)
|
|
40
|
+
.http2(true)
|
|
41
|
+
.build(),
|
|
42
|
+
)
|
|
43
|
+
.build()
|
|
44
|
+
.expect("Unable to build client")
|
|
45
|
+
.get(&url)
|
|
46
|
+
.send()
|
|
47
|
+
.await
|
|
48
|
+
.expect("request");
|
|
49
|
+
|
|
50
|
+
assert_eq!(res.status(), wreq::StatusCode::OK);
|
|
51
|
+
}
|