Repository: seanmonstar/reqwest (Rust)

Issue:
Async request not timing out
I ran `nc -l -p 8001` to start a server that only reads requests (but never responds), and then I connected to it with the sync client and then with the async client, both with a 3 second timeout:

```rust
extern crate reqwest; // 0.9.15
extern crate tokio;   // 0.1.18
use std::time::Duration;
use reqwest::r#async as rasync;
use tokio::prelude::*;

fn main() {
    do_async();
}

fn do_sync() {
    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(3))
        .build().unwrap();
    println!("Sending");
    let resp = client.get("http://localhost:8001").send().unwrap();
    println!("Got response code: {}", resp.status());
}

fn do_async() {
    let mut runtime = tokio::runtime::Builder::new().build().unwrap();
    let client = rasync::Client::builder()
        .timeout(Duration::from_secs(3))
        .build()
        .unwrap();
    println!("Sending");
    let f = client.get("http://localhost:8001")
        .send()
        .map(|r| println!("Got response code: {}", r.status()))
        .map_err(|e| println!("Error: {:?}", e));
    runtime.block_on(f).unwrap();
}
```
Calling `do_sync` will cause the program to fail with a timeout, but calling `do_async` will cause the program to hang indefinitely. I tested this on Linux, but I don't know if it affects Mac or Windows.


Discussion:
It looks like its not implemented: https://github.com/seanmonstar/reqwest/blob/08054a3984a8c73a39cdde17db85d0bb061e333c/src/async_impl/client.rs#L344
@seanmonstar 

> It looks like its not implemented: 

Should it be such simple implementation like bellow,
or there are some underwater rocks, and that's why it is still not implemented?

```diff
diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs
index 7703715..07a7a7b 100644
--- a/src/async_impl/client.rs
+++ b/src/async_impl/client.rs
@@ -26,7 +26,7 @@ use hyper::client::ResponseFuture;
 use mime;
 #[cfg(feature = "default-tls")]
 use native_tls::TlsConnector;
-
+use tokio::prelude::FutureExt;
 
 use super::request::{Request, RequestBuilder};
 use super::response::Response;
@@ -220,6 +220,9 @@ impl ClientBuilder {
                 proxies,
                 proxies_maybe_http_auth,
                 cookie_store,
+                timeout: config
+                .timeout
+                .unwrap_or_else(|| Duration::new(std::u64::MAX, 0)),
             }),
         })
     }
@@ -519,7 +522,16 @@ impl Client {
     /// This method fails if there was an error while sending request,
     /// redirect loop was detected or redirect limit was exhausted.
     pub fn execute(&self, request: Request) -> impl Future<Item = Response, Error = ::Error> {
+        let err_url = request.url().clone();
         self.execute_request(request)
+            .timeout(self.inner.timeout)
+            .map_err(move |err: tokio::timer::timeout::Error<::Error>| {
+                if let Some(inn_err) = err.into_inner() {
+                    inn_err
+                } else {
+                    ::error::timedout(Some(err_url))
+                }
+            })
     }
 
 
@@ -662,6 +674,7 @@ struct ClientRef {
     proxies: Arc<Vec<Proxy>>,
     proxies_maybe_http_auth: bool,
     cookie_store: Option<RwLock<cookie::CookieStore>>,
+    timeout: Duration,
 }
 
 pub struct Pending {
```
One of the underwater rocks is that using a tokio timeout will fail if the future isn't run on a runtime with a tokio-timer.
Having the same issue here. 



Patch:
diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs
index 770371569..f9a4ce22f 100644
--- a/src/async_impl/client.rs
+++ b/src/async_impl/client.rs
@@ -26,6 +26,7 @@ use hyper::client::ResponseFuture;
 use mime;
 #[cfg(feature = "default-tls")]
 use native_tls::TlsConnector;
+use tokio::{clock, timer::Delay};
 
 
 use super::request::{Request, RequestBuilder};
@@ -212,14 +213,15 @@ impl ClientBuilder {
 
         Ok(Client {
             inner: Arc::new(ClientRef {
+                cookie_store,
                 gzip: config.gzip,
                 hyper: hyper_client,
                 headers: config.headers,
                 redirect_policy: config.redirect_policy,
                 referer: config.referer,
+                request_timeout: config.timeout,
                 proxies,
                 proxies_maybe_http_auth,
-                cookie_store,
             }),
         })
     }
@@ -341,16 +343,20 @@ impl ClientBuilder {
         self
     }
 
-    // Currently not used, so hide from docs.
-    #[doc(hidden)]
+    /// Enables a request timeout.
+    ///
+    /// The timeout is applied from the when the request starts connecting
+    /// until the response headers are received. Bodies are not affected.
+    ///
+    /// Default is no timeout.
     pub fn timeout(mut self, timeout: Duration) -> ClientBuilder {
         self.config.timeout = Some(timeout);
         self
     }
 
     /// Sets the maximum idle connection per host allowed in the pool.
-    //
-    // Default is usize::MAX (no limit).
+    ///
+    /// Default is usize::MAX (no limit).
     pub fn max_idle_per_host(mut self, max: usize) -> ClientBuilder {
         self.config.max_idle_per_host = max;
         self
@@ -585,6 +591,10 @@ impl Client {
 
         let in_flight = self.inner.hyper.request(req);
 
+        let timeout = self.inner.request_timeout.map(|dur| {
+            Delay::new(clock::now() + dur)
+        });
+
         Pending {
             inner: PendingInner::Request(PendingRequest {
                 method: method,
@@ -597,6 +607,7 @@ impl Client {
                 client: self.inner.clone(),
 
                 in_flight: in_flight,
+                timeout,
             }),
         }
     }
@@ -654,17 +665,18 @@ impl fmt::Debug for ClientBuilder {
 }
 
 struct ClientRef {
+    cookie_store: Option<RwLock<cookie::CookieStore>>,
     gzip: bool,
     headers: HeaderMap,
     hyper: HyperClient,
     redirect_policy: RedirectPolicy,
     referer: bool,
+    request_timeout: Option<Duration>,
     proxies: Arc<Vec<Proxy>>,
     proxies_maybe_http_auth: bool,
-    cookie_store: Option<RwLock<cookie::CookieStore>>,
 }
 
-pub struct Pending {
+pub(super) struct Pending {
     inner: PendingInner,
 }
 
@@ -684,6 +696,7 @@ struct PendingRequest {
     client: Arc<ClientRef>,
 
     in_flight: ResponseFuture,
+    timeout: Option<Delay>,
 }
 
 impl Pending {
@@ -711,6 +724,12 @@ impl Future for PendingRequest {
     type Error = ::Error;
 
     fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
+        if let Some(ref mut delay) = self.timeout {
+            if let Async::Ready(()) = try_!(delay.poll(), &self.url) {
+                return Err(::error::timedout(Some(self.url.clone())));
+            }
+        }
+
         loop {
             let res = match try_!(self.in_flight.poll(), &self.url) {
                 Async::Ready(res) => res,
diff --git a/src/error.rs b/src/error.rs
index b7e88f2bc..a9241e644 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -150,7 +150,8 @@ impl Error {
             Kind::RedirectLoop |
             Kind::ClientError(_) |
             Kind::ServerError(_) |
-            Kind::UnknownProxyScheme => None,
+            Kind::UnknownProxyScheme |
+            Kind::Timer => None,
         }
     }
 
@@ -275,6 +276,7 @@ impl fmt::Display for Error {
                 fmt::Display::fmt(code, f)
             }
             Kind::UnknownProxyScheme => f.write_str("Unknown proxy scheme"),
+            Kind::Timer => f.write_str("timer unavailable"),
         }
     }
 }
@@ -303,6 +305,7 @@ impl StdError for Error {
             Kind::ClientError(_) => "Client Error",
             Kind::ServerError(_) => "Server Error",
             Kind::UnknownProxyScheme => "Unknown proxy scheme",
+            Kind::Timer => "timer unavailable",
         }
     }
 
@@ -329,7 +332,8 @@ impl StdError for Error {
             Kind::RedirectLoop |
             Kind::ClientError(_) |
             Kind::ServerError(_) |
-            Kind::UnknownProxyScheme => None,
+            Kind::UnknownProxyScheme |
+            Kind::Timer => None,
         }
     }
 }
@@ -357,6 +361,7 @@ pub(crate) enum Kind {
     ClientError(StatusCode),
     ServerError(StatusCode),
     UnknownProxyScheme,
+    Timer,
 }
 
 
@@ -433,6 +438,12 @@ where T: Into<Kind> {
     }
 }
 
+impl From<::tokio::timer::Error> for Kind {
+    fn from(_err: ::tokio::timer::Error) -> Kind {
+        Kind::Timer
+    }
+}
+
 fn io_timeout() -> io::Error {
     io::Error::new(io::ErrorKind::TimedOut, "timed out")
 }


Test patch:
diff --git a/tests/async.rs b/tests/async.rs
index 7c30e12ff..e9446067c 100644
--- a/tests/async.rs
+++ b/tests/async.rs
@@ -7,24 +7,27 @@ extern crate tokio;
 #[macro_use]
 mod support;
 
-use reqwest::async::Client;
-use reqwest::async::multipart::{Form, Part};
-use futures::{Future, Stream};
 use std::io::Write;
 use std::time::Duration;
 
+use futures::{Future, Stream};
+use tokio::runtime::current_thread::Runtime;
+
+use reqwest::async::Client;
+use reqwest::async::multipart::{Form, Part};
+
 #[test]
-fn async_test_gzip_response() {
-    test_gzip(10_000, 4096);
+fn gzip_response() {
+    gzip_case(10_000, 4096);
 }
 
 #[test]
-fn async_test_gzip_single_byte_chunks() {
-    test_gzip(10, 1);
+fn gzip_single_byte_chunks() {
+    gzip_case(10, 1);
 }
 
 #[test]
-fn async_test_multipart() {
+fn multipart() {
     let _ = env_logger::try_init();
 
     let stream = futures::stream::once::<_, hyper::Error>(Ok(hyper::Chunk::from("part1 part2".to_owned())));
@@ -78,7 +81,7 @@ fn async_test_multipart() {
 
     let url = format!("http://{}/multipart/1", server.addr());
 
-    let mut rt = tokio::runtime::current_thread::Runtime::new().expect("new rt");
+    let mut rt = Runtime::new().expect("new rt");
 
     let client = Client::new();
 
@@ -95,7 +98,47 @@ fn async_test_multipart() {
     rt.block_on(res_future).unwrap();
 }
 
-fn test_gzip(response_size: usize, chunk_size: usize) {
+#[test]
+fn request_timeout() {
+    let _ = env_logger::try_init();
+
+    let server = server! {
+        request: b"\
+            GET /slow HTTP/1.1\r\n\
+            user-agent: $USERAGENT\r\n\
+            accept: */*\r\n\
+            accept-encoding: gzip\r\n\
+            host: $HOST\r\n\
+            \r\n\
+            ",
+        response: b"\
+            HTTP/1.1 200 OK\r\n\
+            Content-Length: 5\r\n\
+            \r\n\
+            Hello\
+            ",
+        read_timeout: Duration::from_secs(2)
+    };
+
+    let mut rt = Runtime::new().expect("new rt");
+
+    let client = Client::builder()
+        .timeout(Duration::from_millis(500))
+        .build()
+        .unwrap();
+
+    let url = format!("http://{}/slow", server.addr());
+    let fut = client
+        .get(&url)
+        .send();
+
+    let err = rt.block_on(fut).unwrap_err();
+
+    assert!(err.is_timeout());
+    assert_eq!(err.url().map(|u| u.as_str()), Some(url.as_str()));
+}
+
+fn gzip_case(response_size: usize, chunk_size: usize) {
     let content: String = (0..response_size).into_iter().map(|i| format!("test {}", i)).collect();
     let mut encoder = ::libflate::gzip::Encoder::new(Vec::new()).unwrap();
     match encoder.write(content.as_bytes()) {
@@ -128,7 +171,7 @@ fn test_gzip(response_size: usize, chunk_size: usize) {
         response: response
     };
 
-    let mut rt = tokio::runtime::current_thread::Runtime::new().expect("new rt");
+    let mut rt = Runtime::new().expect("new rt");
 
     let client = Client::new();