Skip to content

Commit 49340d0

Browse files
seanmonstarNutomic
authored andcommitted
feat: add ClientBuilder::read_timeout(dur) (seanmonstar#2241)
1 parent f7ff5fd commit 49340d0

File tree

4 files changed

+257
-32
lines changed

4 files changed

+257
-32
lines changed

src/async_impl/body.rs

Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ use std::fmt;
22
use std::future::Future;
33
use std::pin::Pin;
44
use std::task::{Context, Poll};
5+
use std::time::Duration;
56

67
use bytes::Bytes;
78
use http_body::Body as HttpBody;
89
use http_body_util::combinators::BoxBody;
910
//use sync_wrapper::SyncWrapper;
11+
use pin_project_lite::pin_project;
1012
#[cfg(feature = "stream")]
1113
use tokio::fs::File;
1214
use tokio::time::Sleep;
@@ -23,13 +25,26 @@ enum Inner {
2325
Streaming(BoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>>),
2426
}
2527

26-
/// A body with a total timeout.
27-
///
28-
/// The timeout does not reset upon each chunk, but rather requires the whole
29-
/// body be streamed before the deadline is reached.
30-
pub(crate) struct TotalTimeoutBody<B> {
31-
inner: B,
32-
timeout: Pin<Box<Sleep>>,
28+
pin_project! {
29+
/// A body with a total timeout.
30+
///
31+
/// The timeout does not reset upon each chunk, but rather requires the whole
32+
/// body be streamed before the deadline is reached.
33+
pub(crate) struct TotalTimeoutBody<B> {
34+
#[pin]
35+
inner: B,
36+
timeout: Pin<Box<Sleep>>,
37+
}
38+
}
39+
40+
pin_project! {
41+
pub(crate) struct ReadTimeoutBody<B> {
42+
#[pin]
43+
inner: B,
44+
#[pin]
45+
sleep: Option<Sleep>,
46+
timeout: Duration,
47+
}
3348
}
3449

3550
/// Converts any `impl Body` into a `impl Stream` of just its DATA frames.
@@ -289,23 +304,32 @@ pub(crate) fn total_timeout<B>(body: B, timeout: Pin<Box<Sleep>>) -> TotalTimeou
289304
}
290305
}
291306

307+
pub(crate) fn with_read_timeout<B>(body: B, timeout: Duration) -> ReadTimeoutBody<B> {
308+
ReadTimeoutBody {
309+
inner: body,
310+
sleep: None,
311+
timeout,
312+
}
313+
}
314+
292315
impl<B> hyper::body::Body for TotalTimeoutBody<B>
293316
where
294-
B: hyper::body::Body + Unpin,
317+
B: hyper::body::Body,
295318
B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
296319
{
297320
type Data = B::Data;
298321
type Error = crate::Error;
299322

300323
fn poll_frame(
301-
mut self: Pin<&mut Self>,
324+
self: Pin<&mut Self>,
302325
cx: &mut Context,
303326
) -> Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
304-
if let Poll::Ready(()) = self.timeout.as_mut().poll(cx) {
327+
let this = self.project();
328+
if let Poll::Ready(()) = this.timeout.as_mut().poll(cx) {
305329
return Poll::Ready(Some(Err(crate::error::body(crate::error::TimedOut))));
306330
}
307331
Poll::Ready(
308-
futures_core::ready!(Pin::new(&mut self.inner).poll_frame(cx))
332+
futures_core::ready!(this.inner.poll_frame(cx))
309333
.map(|opt_chunk| opt_chunk.map_err(crate::error::body)),
310334
)
311335
}
@@ -321,22 +345,79 @@ where
321345
}
322346
}
323347

348+
impl<B> hyper::body::Body for ReadTimeoutBody<B>
349+
where
350+
B: hyper::body::Body,
351+
B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
352+
{
353+
type Data = B::Data;
354+
type Error = crate::Error;
355+
356+
fn poll_frame(
357+
self: Pin<&mut Self>,
358+
cx: &mut Context,
359+
) -> Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
360+
let mut this = self.project();
361+
362+
// Start the `Sleep` if not active.
363+
let sleep_pinned = if let Some(some) = this.sleep.as_mut().as_pin_mut() {
364+
some
365+
} else {
366+
this.sleep.set(Some(tokio::time::sleep(*this.timeout)));
367+
this.sleep.as_mut().as_pin_mut().unwrap()
368+
};
369+
370+
// Error if the timeout has expired.
371+
if let Poll::Ready(()) = sleep_pinned.poll(cx) {
372+
return Poll::Ready(Some(Err(crate::error::body(crate::error::TimedOut))));
373+
}
374+
375+
let item = futures_core::ready!(this.inner.poll_frame(cx))
376+
.map(|opt_chunk| opt_chunk.map_err(crate::error::body));
377+
// a ready frame means timeout is reset
378+
this.sleep.set(None);
379+
Poll::Ready(item)
380+
}
381+
382+
#[inline]
383+
fn size_hint(&self) -> http_body::SizeHint {
384+
self.inner.size_hint()
385+
}
386+
387+
#[inline]
388+
fn is_end_stream(&self) -> bool {
389+
self.inner.is_end_stream()
390+
}
391+
}
392+
324393
pub(crate) type ResponseBody =
325394
http_body_util::combinators::BoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>>;
326395

327396
pub(crate) fn response(
328397
body: hyper::body::Incoming,
329-
timeout: Option<Pin<Box<Sleep>>>,
398+
deadline: Option<Pin<Box<Sleep>>>,
399+
read_timeout: Option<Duration>,
330400
) -> ResponseBody {
331401
use http_body_util::BodyExt;
332402

333-
if let Some(timeout) = timeout {
334-
total_timeout(body, timeout).map_err(Into::into).boxed()
335-
} else {
336-
body.map_err(Into::into).boxed()
403+
match (deadline, read_timeout) {
404+
(Some(total), Some(read)) => {
405+
let body = with_read_timeout(body, read).map_err(box_err);
406+
total_timeout(body, total).map_err(box_err).boxed()
407+
}
408+
(Some(total), None) => total_timeout(body, total).map_err(box_err).boxed(),
409+
(None, Some(read)) => with_read_timeout(body, read).map_err(box_err).boxed(),
410+
(None, None) => body.map_err(box_err).boxed(),
337411
}
338412
}
339413

414+
fn box_err<E>(err: E) -> Box<dyn std::error::Error + Send + Sync>
415+
where
416+
E: Into<Box<dyn std::error::Error + Send + Sync>>,
417+
{
418+
err.into()
419+
}
420+
340421
// ===== impl DataStream =====
341422

342423
impl<B> futures_core::Stream for DataStream<B>

src/async_impl/client.rs

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ struct Config {
108108
auto_sys_proxy: bool,
109109
redirect_policy: redirect::Policy,
110110
referer: bool,
111+
read_timeout: Option<Duration>,
111112
timeout: Option<Duration>,
112113
#[cfg(feature = "__tls")]
113114
root_certs: Vec<Certificate>,
@@ -205,6 +206,7 @@ impl ClientBuilder {
205206
auto_sys_proxy: true,
206207
redirect_policy: redirect::Policy::default(),
207208
referer: true,
209+
read_timeout: None,
208210
timeout: None,
209211
#[cfg(feature = "__tls")]
210212
root_certs: Vec::new(),
@@ -741,6 +743,7 @@ impl ClientBuilder {
741743
headers: config.headers,
742744
redirect_policy: config.redirect_policy,
743745
referer: config.referer,
746+
read_timeout: config.read_timeout,
744747
request_timeout: config.timeout,
745748
proxies,
746749
proxies_maybe_http_auth,
@@ -1038,17 +1041,29 @@ impl ClientBuilder {
10381041

10391042
// Timeout options
10401043

1041-
/// Enables a request timeout.
1044+
/// Enables a total request timeout.
10421045
///
10431046
/// The timeout is applied from when the request starts connecting until the
1044-
/// response body has finished.
1047+
/// response body has finished. Also considered a total deadline.
10451048
///
10461049
/// Default is no timeout.
10471050
pub fn timeout(mut self, timeout: Duration) -> ClientBuilder {
10481051
self.config.timeout = Some(timeout);
10491052
self
10501053
}
10511054

1055+
/// Enables a read timeout.
1056+
///
1057+
/// The timeout applies to each read operation, and resets after a
1058+
/// successful read. This is more appropriate for detecting stalled
1059+
/// connections when the size isn't known beforehand.
1060+
///
1061+
/// Default is no timeout.
1062+
pub fn read_timeout(mut self, timeout: Duration) -> ClientBuilder {
1063+
self.config.read_timeout = Some(timeout);
1064+
self
1065+
}
1066+
10521067
/// Set a timeout for only the connect phase of a `Client`.
10531068
///
10541069
/// Default is `None`.
@@ -1995,11 +2010,17 @@ impl Client {
19952010
}
19962011
};
19972012

1998-
let timeout = timeout
2013+
let total_timeout = timeout
19992014
.or(self.inner.request_timeout)
20002015
.map(tokio::time::sleep)
20012016
.map(Box::pin);
20022017

2018+
let read_timeout_fut = self
2019+
.inner
2020+
.read_timeout
2021+
.map(tokio::time::sleep)
2022+
.map(Box::pin);
2023+
20032024
Pending {
20042025
inner: PendingInner::Request(PendingRequest {
20052026
method,
@@ -2014,7 +2035,9 @@ impl Client {
20142035
client: self.inner.clone(),
20152036

20162037
in_flight,
2017-
timeout,
2038+
total_timeout,
2039+
read_timeout_fut,
2040+
read_timeout: self.inner.read_timeout,
20182041
}),
20192042
}
20202043
}
@@ -2220,6 +2243,7 @@ struct ClientRef {
22202243
redirect_policy: redirect::Policy,
22212244
referer: bool,
22222245
request_timeout: Option<Duration>,
2246+
read_timeout: Option<Duration>,
22232247
proxies: Arc<Vec<Proxy>>,
22242248
proxies_maybe_http_auth: bool,
22252249
error_for_status: bool,
@@ -2261,6 +2285,9 @@ impl ClientRef {
22612285
if self.error_for_status {
22622286
f.field("error_for_status", &true);
22632287
}
2288+
if let Some(ref d) = self.read_timeout {
2289+
f.field("read_timeout", d);
2290+
}
22642291
}
22652292
}
22662293

@@ -2292,7 +2319,10 @@ pin_project! {
22922319
#[pin]
22932320
in_flight: ResponseFuture,
22942321
#[pin]
2295-
timeout: Option<Pin<Box<Sleep>>>,
2322+
total_timeout: Option<Pin<Box<Sleep>>>,
2323+
#[pin]
2324+
read_timeout_fut: Option<Pin<Box<Sleep>>>,
2325+
read_timeout: Option<Duration>,
22962326
}
22972327
}
22982328

@@ -2307,8 +2337,12 @@ impl PendingRequest {
23072337
self.project().in_flight
23082338
}
23092339

2310-
fn timeout(self: Pin<&mut Self>) -> Pin<&mut Option<Pin<Box<Sleep>>>> {
2311-
self.project().timeout
2340+
fn total_timeout(self: Pin<&mut Self>) -> Pin<&mut Option<Pin<Box<Sleep>>>> {
2341+
self.project().total_timeout
2342+
}
2343+
2344+
fn read_timeout(self: Pin<&mut Self>) -> Pin<&mut Option<Pin<Box<Sleep>>>> {
2345+
self.project().read_timeout_fut
23122346
}
23132347

23142348
fn urls(self: Pin<&mut Self>) -> &mut Vec<Url> {
@@ -2445,7 +2479,15 @@ impl Future for PendingRequest {
24452479
type Output = Result<Response, crate::Error>;
24462480

24472481
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
2448-
if let Some(delay) = self.as_mut().timeout().as_mut().as_pin_mut() {
2482+
if let Some(delay) = self.as_mut().total_timeout().as_mut().as_pin_mut() {
2483+
if let Poll::Ready(()) = delay.poll(cx) {
2484+
return Poll::Ready(Err(
2485+
crate::error::request(crate::error::TimedOut).with_url(self.url.clone())
2486+
));
2487+
}
2488+
}
2489+
2490+
if let Some(delay) = self.as_mut().read_timeout().as_mut().as_pin_mut() {
24492491
if let Poll::Ready(()) = delay.poll(cx) {
24502492
return Poll::Ready(Err(
24512493
crate::error::request(crate::error::TimedOut).with_url(self.url.clone())
@@ -2637,7 +2679,8 @@ impl Future for PendingRequest {
26372679
res,
26382680
self.url.clone(),
26392681
self.client.accepts,
2640-
self.timeout.take(),
2682+
self.total_timeout.take(),
2683+
self.read_timeout,
26412684
);
26422685

26432686
if self.client.error_for_status {

src/async_impl/response.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::fmt;
22
use std::net::SocketAddr;
33
use std::pin::Pin;
4+
use std::time::Duration;
45

56
use bytes::Bytes;
67
use http_body_util::BodyExt;
@@ -37,12 +38,13 @@ impl Response {
3738
res: hyper::Response<hyper::body::Incoming>,
3839
url: Url,
3940
accepts: Accepts,
40-
timeout: Option<Pin<Box<Sleep>>>,
41+
total_timeout: Option<Pin<Box<Sleep>>>,
42+
read_timeout: Option<Duration>,
4143
) -> Response {
4244
let (mut parts, body) = res.into_parts();
4345
let decoder = Decoder::detect(
4446
&mut parts.headers,
45-
super::body::response(body, timeout),
47+
super::body::response(body, total_timeout, read_timeout),
4648
accepts,
4749
);
4850
let res = hyper::Response::from_parts(parts, decoder);

0 commit comments

Comments
 (0)