Repository: seanmonstar/reqwest (language: rust)

Issue:
CookieStore
This a very much WIP start for #14.

It contains a working implementation of the cookie store for the AsyncClient, an accessor on the Response and a wrapper type for `Cookie` but no tests and no methods on the `CookieStore` wrapper.

Before I go any further there are a few questions / discussion points:

* I've added the store to the client via a `Option<RwLock<CookieStore>>`, which seems most sensible to me since we need a read-lock before sending a request and a write lock after receiving.

* Naming: I added the `cookies` top level module. I'd prefer `cookie` but that's already the name of the dependency. I don't think there is a way to disambiguate a top level module and a dependency in the 2015 edition, is there? (would be possible with 2018 I think). We could rename the dependency to something, but that is a fairly new Rust feature.

* Is it OK to use `impl Iterator<...>` as a return value for Response::cookies? (I reckon this depends on the minimum supported Rust version) Otherwise we'd need a custom Iterator type or just return a `Vec`.

* Expiration: `cookie::Cookie` relies on the old `time` crate to handle expiration time. How would you like to handle this? 
  - the canonical crate would be `chrono`, but that's a heavy new dependency. 
  - `time` is probably not a good choice for a public dependency
  - the best Option might be a opaque `ExpirationTime` type that only converts to/from unix epoch as u64 (or `std::time::SystemTime`).

* Wrapping `Cookie` is easy, but `CookieStore` is a bit more awkward: we'd need something like a `CookieRef` type to represent a cookie borrowed from the store (for `get`, `iter`, ... methods)

* Should a `Request` / `RequestBuilder` have an easy way to manually add cookies? As in a `RequestBuilder::cookie()` method? It would be convenient. The only little stumbling block here is that duplicate cookie values already present in the  store would need to be dropped. Also, what should be the behaviour when manually adding a `Cookie` header after adding some cookies with the builder? Drop the manually added ones? Merge together?

* As the TODO in the code states, must verify that the `cookie::EncodedCookie` type and Display implementation guarantees valid ASCII only header so the `unwrap()` is safe to use. ( it should )

* How much of the API surface from `cookie_store::CookieStore` is desirable to include? Especially regarding the load/save methods


Discussion / hints:

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index 282d8e7cb..97ac6e09a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -34,14 +34,14 @@ matrix:
           env: FEATURES="--features trust-dns"
 
         # android
-        - rust: stable
-          env: TARGET=aarch64-linux-android
-          install: rustup target add "$TARGET"
-          # disable default-tls feature since cross-compiling openssl is dragons
-          script: cargo build --target "$TARGET" --no-default-features
+        #- rust: stable
+        #  env: TARGET=aarch64-linux-android
+        #  install: rustup target add "$TARGET"
+        #  # disable default-tls feature since cross-compiling openssl is dragons
+        #  script: cargo build --target "$TARGET" --no-default-features
 
         # minimum version
-        - rust: 1.30.0
+        - rust: 1.31.0
           script: cargo build
 
 sudo: false
diff --git a/Cargo.toml b/Cargo.toml
index d226b7c4e..05acd497f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,6 +10,9 @@ readme = "README.md"
 license = "MIT/Apache-2.0"
 categories = ["web-programming::http-client"]
 
+[package.metadata.docs.rs]
+all-features = true
+
 [dependencies]
 base64 = "0.10"
 bytes = "0.4"
@@ -43,6 +46,9 @@ socks = { version = "0.3.2", optional = true }
 tokio-rustls = { version = "0.9", optional = true }
 trust-dns-resolver = { version = "0.10", optional = true }
 webpki-roots = { version = "0.16", optional = true }
+cookie_store = "0.5.1"
+cookie = "0.11.0"
+time = "0.1.42"
 
 [dev-dependencies]
 env_logger = "0.6"
@@ -63,6 +69,3 @@ rustls-tls = ["hyper-rustls", "tokio-rustls", "webpki-roots", "rustls", "tls"]
 trust-dns = ["trust-dns-resolver"]
 
 hyper-011 = ["hyper-old-types"]
-
-[package.metadata.docs.rs]
-all-features = true
diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs
index 94ac612f4..d1b96f8c8 100644
--- a/src/async_impl/client.rs
+++ b/src/async_impl/client.rs
@@ -1,5 +1,5 @@
 use std::{fmt, str};
-use std::sync::Arc;
+use std::sync::{Arc, RwLock};
 use std::time::Duration;
 use std::net::IpAddr;
 
@@ -31,6 +31,7 @@ use super::request::{Request, RequestBuilder};
 use super::response::Response;
 use connect::Connector;
 use into_url::{expect_uri, try_uri};
+use cookie;
 use redirect::{self, RedirectPolicy, remove_sensitive_headers};
 use {IntoUrl, Method, Proxy, StatusCode, Url};
 #[cfg(feature = "tls")]
@@ -82,6 +83,7 @@ struct Config {
     http1_title_case_headers: bool,
     local_address: Option<IpAddr>,
     nodelay: bool,
+    cookie_store: Option<cookie::CookieStore>,
 }
 
 impl ClientBuilder {
@@ -116,7 +118,8 @@ impl ClientBuilder {
                 http2_only: false,
                 http1_title_case_headers: false,
                 local_address: None,
-                nodelay: false
+                nodelay: false,
+                cookie_store: None,
             },
         }
     }
@@ -204,6 +207,8 @@ impl ClientBuilder {
             .iter()
             .any(|p| p.maybe_has_http_auth());
 
+        let cookie_store = config.cookie_store.map(RwLock::new);
+
         Ok(Client {
             inner: Arc::new(ClientRef {
                 gzip: config.gzip,
@@ -213,6 +218,7 @@ impl ClientBuilder {
                 referer: config.referer,
                 proxies,
                 proxies_maybe_http_auth,
+                cookie_store,
             }),
         })
     }
@@ -388,6 +394,21 @@ impl ClientBuilder {
         self.config.local_address = addr.into();
         self
     }
+
+    /// Enable a persistent cookie store for the client.
+    /// 
+    /// Cookies received in responses will be preserved and included in 
+    /// additional requests.
+    /// 
+    /// By default, no cookie store is used.
+    pub fn cookie_store(mut self, enable: bool) -> ClientBuilder {
+        self.config.cookie_store = if enable {
+            Some(cookie::CookieStore::default())
+        } else {
+            None
+        };
+        self
+    }
 }
 
 type HyperClient = ::hyper::Client<Connector>;
@@ -514,6 +535,22 @@ impl Client {
             headers.insert(key, value.clone());
         }
 
+        // Add cookies from the cookie store.
+        if let Some(cookie_store_wrapper) = self.inner.cookie_store.as_ref() {
+            if headers.get(::header::COOKIE).is_none() {
+                let cookie_store = cookie_store_wrapper.read().unwrap();
+                let header = cookie_store
+                    .0
+                    .get_request_cookies(&url)
+                    .map(|c| c.encoded().to_string())
+                    .collect::<Vec<_>>()
+                    .join("; ");
+                if !header.is_empty() {
+                    headers.insert(::header::COOKIE, HeaderValue::from_bytes(header.as_bytes()).unwrap());
+                }
+            }
+        }
+
         if self.inner.gzip &&
             !headers.contains_key(ACCEPT_ENCODING) &&
             !headers.contains_key(RANGE) {
@@ -620,6 +657,7 @@ struct ClientRef {
     referer: bool,
     proxies: Arc<Vec<Proxy>>,
     proxies_maybe_http_auth: bool,
+    cookie_store: Option<RwLock<cookie::CookieStore>>,
 }
 
 pub struct Pending {
@@ -674,6 +712,13 @@ impl Future for PendingRequest {
                 Async::Ready(res) => res,
                 Async::NotReady => return Ok(Async::NotReady),
             };
+            if let Some(store_wrapper) = self.client.cookie_store.as_ref() {
+                let mut store = store_wrapper.write().unwrap();
+                let cookies = cookie::extract_response_cookies(&res.headers())
+                    .filter_map(|res| res.ok())
+                    .map(|cookie| cookie.into_inner().into_owned());
+                store.0.store_response_cookies(cookies, &self.url);
+            }
             let should_redirect = match res.status() {
                 StatusCode::MOVED_PERMANENTLY |
                 StatusCode::FOUND |
diff --git a/src/async_impl/response.rs b/src/async_impl/response.rs
index e727eb8e0..368b6a9c9 100644
--- a/src/async_impl/response.rs
+++ b/src/async_impl/response.rs
@@ -13,6 +13,7 @@ use serde_json;
 use url::Url;
 use http;
 
+use cookie;
 use super::Decoder;
 use super::body::Body;
 
@@ -69,6 +70,14 @@ impl Response {
         &mut self.headers
     }
 
+    /// Retrieve the cookies contained in the response.
+    /// 
+    /// Note that invalid 'Set-Cookie' headers will be ignored.
+    pub fn cookies<'a>(&'a self) -> impl Iterator<Item = cookie::Cookie<'a>> + 'a {
+        cookie::extract_response_cookies(&self.headers)
+            .filter_map(Result::ok)
+    }
+
     /// Get the final `Url` of this `Response`.
     #[inline]
     pub fn url(&self) -> &Url {
diff --git a/src/client.rs b/src/client.rs
index 2cfd23844..763319403 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -366,6 +366,16 @@ impl ClientBuilder {
     {
         self.with_inner(move |inner| inner.local_address(addr))
     }
+
+    /// Enable a persistent cookie store for the client.
+    /// 
+    /// Cookies received in responses will be preserved and included in 
+    /// additional requests.
+    /// 
+    /// By default, no cookie store is used.
+    pub fn cookie_store(self, enable: bool) -> ClientBuilder {
+        self.with_inner(|inner| inner.cookie_store(enable))
+    }
 }
 
 
diff --git a/src/cookie.rs b/src/cookie.rs
new file mode 100644
index 000000000..26f3ec3ba
--- /dev/null
+++ b/src/cookie.rs
@@ -0,0 +1,138 @@
+//! The cookies module contains types for working with request and response cookies.
+
+use cookie_crate;
+use header;
+use std::borrow::Cow;
+use std::fmt;
+use std::time::SystemTime;
+
+/// Convert a time::Tm time to SystemTime.
+fn tm_to_systemtime(tm: ::time::Tm) -> SystemTime {
+    let seconds = tm.to_timespec().sec;
+    let duration = std::time::Duration::from_secs(seconds.abs() as u64);
+    if seconds > 0 {
+        SystemTime::UNIX_EPOCH + duration
+    } else {
+        SystemTime::UNIX_EPOCH - duration
+    }
+}
+
+/// Error representing a parse failure of a 'Set-Cookie' header.
+pub struct CookieParseError(cookie::ParseError);
+
+impl<'a> fmt::Debug for CookieParseError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl<'a> fmt::Display for CookieParseError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl std::error::Error for CookieParseError {}
+
+/// A single HTTP cookie.
+pub struct Cookie<'a>(cookie::Cookie<'a>);
+
+impl<'a> fmt::Debug for Cookie<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl Cookie<'static> {
+    /// Construct a new cookie with the given name and value.
+    pub fn new<N, V>(name: N, value: V) -> Self
+    where
+        N: Into<Cow<'static, str>>,
+        V: Into<Cow<'static, str>>,
+    {
+        Cookie(cookie::Cookie::new(name, value))
+    }
+}
+
+impl<'a> Cookie<'a> {
+    fn parse(value: &'a ::header::HeaderValue) -> Result<Cookie<'a>, CookieParseError> {
+        std::str::from_utf8(value.as_bytes())
+            .map_err(cookie::ParseError::from)
+            .and_then(cookie::Cookie::parse)
+            .map_err(CookieParseError)
+            .map(Cookie)
+    }
+
+    pub(crate) fn into_inner(self) -> cookie::Cookie<'a> {
+        self.0
+    }
+
+    /// The name of the cookie.
+    pub fn name(&self) -> &str {
+        self.0.name()
+    }
+
+    /// The value of the cookie.
+    pub fn value(&self) -> &str {
+        self.0.value()
+    }
+
+    /// Returns true if the 'HttpOnly' directive is enabled.
+    pub fn http_only(&self) -> bool {
+        self.0.http_only().unwrap_or(false)
+    }
+
+    /// Returns true if the 'Secure' directive is enabled.
+    pub fn secure(&self) -> bool {
+        self.0.secure().unwrap_or(false)
+    }
+
+    /// Returns true if  'SameSite' directive is 'Lax'.
+    pub fn same_site_lax(&self) -> bool {
+        self.0.same_site() == Some(cookie_crate::SameSite::Lax)
+    }
+
+    /// Returns true if  'SameSite' directive is 'Strict'.
+    pub fn same_site_strict(&self) -> bool {
+        self.0.same_site() == Some(cookie_crate::SameSite::Strict)
+    }
+
+    /// Returns the path directive of the cookie, if set.
+    pub fn path(&self) -> Option<&str> {
+        self.0.path()
+    }
+
+    /// Returns the domain directive of the cookie, if set.
+    pub fn domain(&self) -> Option<&str> {
+        self.0.domain()
+    }
+
+    /// Get the Max-Age information.
+    pub fn max_age(&self) -> Option<std::time::Duration> {
+        self.0.max_age().map(|d| std::time::Duration::new(d.num_seconds() as u64, 0))
+    }
+
+    /// The cookie expiration time.
+    pub fn expires(&self) -> Option<SystemTime> {
+        self.0.expires().map(tm_to_systemtime)
+    }
+}
+
+pub(crate) fn extract_response_cookies<'a>(
+    headers: &'a hyper::HeaderMap,
+) -> impl Iterator<Item = Result<Cookie<'a>, CookieParseError>> + 'a {
+    headers
+        .get_all(header::SET_COOKIE)
+        .iter()
+        .map(|value| Cookie::parse(value))
+}
+
+/// A persistent cookie store that provides session support.
+#[derive(Default)]
+pub(crate) struct CookieStore(pub(crate) ::cookie_store::CookieStore);
+
+impl<'a> fmt::Debug for CookieStore {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 6fe715705..979f78bad 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -173,6 +173,8 @@
 
 extern crate base64;
 extern crate bytes;
+extern crate cookie as cookie_crate;
+extern crate cookie_store;
 extern crate encoding_rs;
 #[macro_use]
 extern crate futures;
@@ -195,6 +197,7 @@ extern crate serde;
 extern crate serde_derive;
 extern crate serde_json;
 extern crate serde_urlencoded;
+extern crate time;
 extern crate tokio;
 #[cfg_attr(feature = "default-tls", macro_use)]
 extern crate tokio_io;
@@ -241,6 +244,7 @@ mod async_impl;
 mod connect;
 mod body;
 mod client;
+pub mod cookie;
 #[cfg(feature = "trust-dns")]
 mod dns;
 mod into_url;
diff --git a/src/response.rs b/src/response.rs
index e2d7e7971..79501f137 100644
--- a/src/response.rs
+++ b/src/response.rs
@@ -12,6 +12,7 @@ use mime::Mime;
 use serde::de::DeserializeOwned;
 use serde_json;
 
+use cookie;
 use client::KeepCoreThreadAlive;
 use hyper::header::HeaderMap;
 use {async_impl, StatusCode, Url, Version, wait};
@@ -122,6 +123,15 @@ impl Response {
         self.inner.headers()
     }
 
+    /// Retrieve the cookies contained in the response.
+    /// 
+    /// Note that invalid 'Set-Cookie' headers will be ignored.
+    pub fn cookies<'a>(&'a self) -> impl Iterator< Item = cookie::Cookie<'a> > + 'a {
+        cookie::extract_response_cookies(self.headers())
+            .filter_map(Result::ok)
+    }
+
+
     /// Get the HTTP `Version` of this `Response`.
     #[inline]
     pub fn version(&self) -> Version {


Test patch:
diff --git a/tests/cookie.rs b/tests/cookie.rs
new file mode 100644
index 000000000..b98306b00
--- /dev/null
+++ b/tests/cookie.rs
@@ -0,0 +1,335 @@
+extern crate reqwest;
+
+#[macro_use]
+mod support;
+
+#[test]
+fn cookie_response_accessor() {
+    let mut rt = tokio::runtime::current_thread::Runtime::new().expect("new rt");
+    let client = reqwest::async::Client::new();
+
+    let server = server! {
+        request: b"\
+            GET / 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\
+            Set-Cookie: key=val\r\n\
+            Set-Cookie: expires=1; Expires=Wed, 21 Oct 2015 07:28:00 GMT\r\n\
+            Set-Cookie: path=1; Path=/the-path\r\n\
+            Set-Cookie: maxage=1; Max-Age=100\r\n\
+            Set-Cookie: domain=1; Domain=mydomain\r\n\
+            Set-Cookie: secure=1; Secure\r\n\
+            Set-Cookie: httponly=1; HttpOnly\r\n\
+            Set-Cookie: samesitelax=1; SameSite=Lax\r\n\
+            Set-Cookie: samesitestrict=1; SameSite=Strict\r\n\
+            Content-Length: 0\r\n\
+            \r\n\
+            "
+    };
+
+    let url = format!("http://{}/", server.addr());
+    let res = rt.block_on(client.get(&url).send()).unwrap();
+
+    let cookies = res.cookies().collect::<Vec<_>>();
+
+    // key=val
+    assert_eq!(cookies[0].name(), "key");
+    assert_eq!(cookies[0].value(), "val");
+
+    // expires
+    assert_eq!(cookies[1].name(), "expires");
+    assert_eq!(
+        cookies[1].expires().unwrap(), 
+        std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1445412480)
+    );
+
+    // path
+    assert_eq!(cookies[2].name(), "path");
+    assert_eq!(cookies[2].path().unwrap(), "/the-path");
+
+    // max-age
+    assert_eq!(cookies[3].name(), "maxage");
+    assert_eq!(cookies[3].max_age().unwrap(), std::time::Duration::from_secs(100));
+
+    // domain
+    assert_eq!(cookies[4].name(), "domain");
+    assert_eq!(cookies[4].domain().unwrap(), "mydomain");
+
+    // secure
+    assert_eq!(cookies[5].name(), "secure");
+    assert_eq!(cookies[5].secure(), true);
+
+    // httponly
+    assert_eq!(cookies[6].name(), "httponly");
+    assert_eq!(cookies[6].http_only(), true);
+
+    // samesitelax
+    assert_eq!(cookies[7].name(), "samesitelax");
+    assert!(cookies[7].same_site_lax());
+
+    // samesitestrict
+    assert_eq!(cookies[8].name(), "samesitestrict");
+    assert!(cookies[8].same_site_strict());
+}
+
+#[test]
+fn cookie_store_simple() {
+    let mut rt = tokio::runtime::current_thread::Runtime::new().expect("new rt");
+    let client = reqwest::async::Client::builder().cookie_store(true).build().unwrap();
+
+    let server = server! {
+        request: b"\
+            GET / 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\
+            Set-Cookie: key=val\r\n\
+            Content-Length: 0\r\n\
+            \r\n\
+            "
+    };
+    let url = format!("http://{}/", server.addr());
+    rt.block_on(client.get(&url).send()).unwrap();
+
+    let server = server! {
+        request: b"\
+            GET / HTTP/1.1\r\n\
+            user-agent: $USERAGENT\r\n\
+            accept: */*\r\n\
+            cookie: key=val\r\n\
+            accept-encoding: gzip\r\n\
+            host: $HOST\r\n\
+            \r\n\
+            ",
+        response: b"\
+            HTTP/1.1 200 OK\r\n\
+            Content-Length: 0\r\n\
+            \r\n\
+            "
+    };
+    let url = format!("http://{}/", server.addr());
+    rt.block_on(client.get(&url).send()).unwrap();
+}
+
+#[test]
+fn cookie_store_overwrite_existing() {
+    let mut rt = tokio::runtime::current_thread::Runtime::new().expect("new rt");
+    let client = reqwest::async::Client::builder().cookie_store(true).build().unwrap();
+
+    let server = server! {
+        request: b"\
+            GET / 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\
+            Set-Cookie: key=val\r\n\
+            Content-Length: 0\r\n\
+            \r\n\
+            "
+    };
+    let url = format!("http://{}/", server.addr());
+    rt.block_on(client.get(&url).send()).unwrap();
+
+    let server = server! {
+        request: b"\
+            GET / HTTP/1.1\r\n\
+            user-agent: $USERAGENT\r\n\
+            accept: */*\r\n\
+            cookie: key=val\r\n\
+            accept-encoding: gzip\r\n\
+            host: $HOST\r\n\
+            \r\n\
+            ",
+        response: b"\
+            HTTP/1.1 200 OK\r\n\
+            Set-Cookie: key=val2\r\n\
+            Content-Length: 0\r\n\
+            \r\n\
+            "
+    };
+    let url = format!("http://{}/", server.addr());
+    rt.block_on(client.get(&url).send()).unwrap();
+
+    let server = server! {
+        request: b"\
+            GET / HTTP/1.1\r\n\
+            user-agent: $USERAGENT\r\n\
+            accept: */*\r\n\
+            cookie: key=val2\r\n\
+            accept-encoding: gzip\r\n\
+            host: $HOST\r\n\
+            \r\n\
+            ",
+        response: b"\
+            HTTP/1.1 200 OK\r\n\
+            Content-Length: 0\r\n\
+            \r\n\
+            "
+    };
+    let url = format!("http://{}/", server.addr());
+    rt.block_on(client.get(&url).send()).unwrap();
+}
+
+#[test]
+fn cookie_store_max_age() {
+    let mut rt = tokio::runtime::current_thread::Runtime::new().expect("new rt");
+    let client = reqwest::async::Client::builder().cookie_store(true).build().unwrap();
+
+    let server = server! {
+        request: b"\
+            GET / 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\
+            Set-Cookie: key=val; Max-Age=0\r\n\
+            Content-Length: 0\r\n\
+            \r\n\
+            "
+    };
+    let url = format!("http://{}/", server.addr());
+    rt.block_on(client.get(&url).send()).unwrap();
+
+    let server = server! {
+        request: b"\
+            GET / 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: 0\r\n\
+            \r\n\
+            "
+    };
+    let url = format!("http://{}/", server.addr());
+    rt.block_on(client.get(&url).send()).unwrap();
+}
+
+#[test]
+fn cookie_store_expires() {
+    let mut rt = tokio::runtime::current_thread::Runtime::new().expect("new rt");
+    let client = reqwest::async::Client::builder().cookie_store(true).build().unwrap();
+
+    let server = server! {
+        request: b"\
+            GET / 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\
+            Set-Cookie: key=val; Expires=Wed, 21 Oct 2015 07:28:00 GMT\r\n\
+            Content-Length: 0\r\n\
+            \r\n\
+            "
+    };
+    let url = format!("http://{}/", server.addr());
+    rt.block_on(client.get(&url).send()).unwrap();
+
+    let server = server! {
+        request: b"\
+            GET / 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: 0\r\n\
+            \r\n\
+            "
+    };
+    let url = format!("http://{}/", server.addr());
+    rt.block_on(client.get(&url).send()).unwrap();
+}
+
+#[test]
+fn cookie_store_path() {
+    let mut rt = tokio::runtime::current_thread::Runtime::new().expect("new rt");
+    let client = reqwest::async::Client::builder().cookie_store(true).build().unwrap();
+
+    let server = server! {
+        request: b"\
+            GET / 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\
+            Set-Cookie: key=val; Path=/subpath\r\n\
+            Content-Length: 0\r\n\
+            \r\n\
+            "
+    };
+    let url = format!("http://{}/", server.addr());
+    rt.block_on(client.get(&url).send()).unwrap();
+
+    let server = server! {
+        request: b"\
+            GET / 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: 0\r\n\
+            \r\n\
+            "
+    };
+    let url = format!("http://{}/", server.addr());
+    rt.block_on(client.get(&url).send()).unwrap();
+
+    let server = server! {
+        request: b"\
+            GET /subpath HTTP/1.1\r\n\
+            user-agent: $USERAGENT\r\n\
+            accept: */*\r\n\
+            cookie: key=val\r\n\
+            accept-encoding: gzip\r\n\
+            host: $HOST\r\n\
+            \r\n\
+            ",
+        response: b"\
+            HTTP/1.1 200 OK\r\n\
+            Content-Length: 0\r\n\
+            \r\n\
+            "
+    };
+    let url = format!("http://{}/subpath", server.addr());
+    rt.block_on(client.get(&url).send()).unwrap();
+}