Repository: seanmonstar/reqwest (language: rust)

Issue:
Feature request: set default headers for every request
And #177 can be resolved once this is implemented.


Discussion / hints:
@seanmonstar If this request is acceptable, I'd like to work on it.
I'd love to hear what you think an exposed API would look like, first. This would be like a set of headers stored in the Client that are applied to all requests?
Yeah, as you said, we could add something like
```rust
Client::set_default_headers(mut self, headers: ::header::Headers) -> Self;
```
which saves the default headers in the `Client` instance, and `request::builder` can inspect it and set the default headers of the request.

To be specific, I plan to set default headers as the following:

```rust
let mut client = reqwest::Client::new();
client.set_default_headers(headers) // for simplicity, only a set of headers is allowed for now
    .post(url)
    .body(body)
    .send()?
```

Alternatively, we could pass an option struct to the constructor of `Client`, like
```rust
let client = reqwest::Client::new(RequestOption {
    headers,
    // base_url,  // I think we could add this later
});
client.get(url).send()?;
...
```

To implement, the `Client` should be extend as something like:
```rust
Client {
    inner: ClientHandle,
    headers: ::header::Headers, 
}
```
then `request::builder` could be rewritten as:

```rust
pub fn builder(client: Client, req: ::Result<Request>) -> RequestBuilder {
    match req {
        Ok(mut req) => {
            req.headers_mut().extend(client.headers.iter());
            RequestBuilder {
                client: client,
                request: Some(req),
                err: None,
            },
        },
        Err(err) => RequestBuilder {
            client: client,
            request: None,
            err: Some(err)
        },
    }
}
```

How is it look like? Did I miss something?
The `Client` can be cloned and given to different threads, so it's probably best to not be able to mutate the `Client` itself. We do have a `ClientBuilder`, however. That seems like a fine place to customize  some default headers.
Perhaps we could save the headers in `async_impl::client::ClientRef` ?
As the eventually place store them, sure. They should probably reside right next to the other stuff that is needed to make a request.

Fix patch:
diff --git a/.gitignore b/.gitignore
index a9d37c560..d4f917d3d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 target
 Cargo.lock
+*.swp
diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs
index 32ab23dc6..9d1d57ce3 100644
--- a/src/async_impl/client.rs
+++ b/src/async_impl/client.rs
@@ -63,6 +63,7 @@ pub struct ClientBuilder {
 
 struct Config {
     gzip: bool,
+    headers: Headers,
     hostname_verification: bool,
     proxies: Vec<Proxy>,
     redirect_policy: RedirectPolicy,
@@ -76,18 +77,25 @@ impl ClientBuilder {
     /// Constructs a new `ClientBuilder`
     pub fn new() -> ClientBuilder {
         match TlsConnector::builder() {
-            Ok(tls_connector_builder) => ClientBuilder {
-                config: Some(Config {
-                    gzip: true,
-                    hostname_verification: true,
-                    proxies: Vec::new(),
-                    redirect_policy: RedirectPolicy::default(),
-                    referer: true,
-                    timeout: None,
-                    tls: tls_connector_builder,
-                    dns_threads: 4,
-                }),
-                err: None,
+            Ok(tls_connector_builder) => {
+                let mut headers = Headers::with_capacity(2);
+                headers.set(UserAgent::new(DEFAULT_USER_AGENT));
+                headers.set(Accept::star());
+
+                ClientBuilder {
+                    config: Some(Config {
+                        gzip: true,
+                        headers: headers,
+                        hostname_verification: true,
+                        proxies: Vec::new(),
+                        redirect_policy: RedirectPolicy::default(),
+                        referer: true,
+                        timeout: None,
+                        tls: tls_connector_builder,
+                        dns_threads: 4,
+                    }),
+                    err: None,
+                }
             },
             Err(e) => ClientBuilder {
                 config: None,
@@ -131,6 +139,7 @@ impl ClientBuilder {
             inner: Arc::new(ClientRef {
                 gzip: config.gzip,
                 hyper: hyper_client,
+                headers: config.headers,
                 proxies: proxies,
                 redirect_policy: config.redirect_policy,
                 referer: config.referer,
@@ -189,6 +198,15 @@ impl ClientBuilder {
         self
     }
 
+    /// Sets the default headers for every request.
+    #[inline]
+    pub fn default_headers(&mut self, headers: Headers) -> &mut ClientBuilder {
+        if let Some(config) = config_mut(&mut self.config, &self.err) {
+            config.headers.extend(headers.iter());
+        }
+        self
+    }
+
     /// Enable auto gzip decompression by checking the ContentEncoding response header.
     ///
     /// Default is enabled.
@@ -372,17 +390,13 @@ impl Client {
         let (
             method,
             url,
-            mut headers,
+            user_headers,
             body
         ) = request::pieces(req);
 
-        if !headers.has::<UserAgent>() {
-            headers.set(UserAgent::new(DEFAULT_USER_AGENT));
-        }
+        let mut headers = self.inner.headers.clone(); // default headers
+        headers.extend(user_headers.iter());
 
-        if !headers.has::<Accept>() {
-            headers.set(Accept::star());
-        }
         if self.inner.gzip &&
             !headers.has::<AcceptEncoding>() &&
             !headers.has::<Range>() {
@@ -442,6 +456,7 @@ impl fmt::Debug for ClientBuilder {
 
 struct ClientRef {
     gzip: bool,
+    headers: Headers,
     hyper: HyperClient,
     proxies: Arc<Vec<Proxy>>,
     redirect_policy: RedirectPolicy,
diff --git a/src/client.rs b/src/client.rs
index 480c8d7e6..11f154d7b 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -8,7 +8,7 @@ use futures::sync::{mpsc, oneshot};
 
 use request::{self, Request, RequestBuilder};
 use response::{self, Response};
-use {async_impl, Certificate, Identity, Method, IntoUrl, Proxy, RedirectPolicy, wait};
+use {async_impl, header, Certificate, Identity, Method, IntoUrl, Proxy, RedirectPolicy, wait};
 
 /// A `Client` to make Requests with.
 ///
@@ -167,6 +167,50 @@ impl ClientBuilder {
         self
     }
 
+    /// Sets the default headers for every request.
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// use reqwest::header;
+    /// # fn build_client() -> Result<(), Box<std::error::Error>> {
+    /// let mut headers = header::Headers::new();
+    /// headers.set(header::Authorization("secret".to_string()));
+    ///
+    /// // get a client builder
+    /// let client = reqwest::Client::builder()
+    ///     .default_headers(headers)
+    ///     .build()?;
+    /// let res = client.get("https://www.rust-lang.org").send()?;
+    /// # Ok(())
+    /// # }
+    /// ```
+    ///
+    /// Override the default headers:
+    ///
+    /// ```rust
+    /// use reqwest::header;
+    /// # fn build_client() -> Result<(), Box<std::error::Error>> {
+    /// let mut headers = header::Headers::new();
+    /// headers.set(header::Authorization("secret".to_string()));
+    ///
+    /// // get a client builder
+    /// let client = reqwest::Client::builder()
+    ///     .default_headers(headers)
+    ///     .build()?;
+    /// let res = client
+    ///     .get("https://www.rust-lang.org")
+    ///     .header(header::Authorization("token".to_string()))
+    ///     .send()?;
+    /// # Ok(())
+    /// # }
+    /// ```
+    #[inline]
+    pub fn default_headers(&mut self, headers: header::Headers) -> &mut ClientBuilder {
+        self.inner.default_headers(headers);
+        self
+    }
+
     /// Enable auto gzip decompression by checking the ContentEncoding response header.
     ///
     /// Default is enabled.


Test patch:
diff --git a/tests/client.rs b/tests/client.rs
index 541f1ba86..a302a2a29 100644
--- a/tests/client.rs
+++ b/tests/client.rs
@@ -79,9 +79,9 @@ fn test_post() {
         request: b"\
             POST /2 HTTP/1.1\r\n\
             Host: $HOST\r\n\
-            Content-Length: 5\r\n\
             User-Agent: $USERAGENT\r\n\
             Accept: */*\r\n\
+            Content-Length: 5\r\n\
             Accept-Encoding: gzip\r\n\
             \r\n\
             Hello\
@@ -170,3 +170,111 @@ fn test_error_for_status_5xx() {
     assert!(err.is_server_error());
     assert_eq!(err.status(), Some(reqwest::StatusCode::InternalServerError));
 }
+
+#[test]
+fn test_default_headers() {
+    use reqwest::header;
+    let mut headers = header::Headers::with_capacity(1);
+    let mut cookies = header::Cookie::new();
+    cookies.set("a", "b");
+    cookies.set("c", "d");
+    headers.set(cookies);
+    let client = reqwest::Client::builder()
+        .default_headers(headers)
+        .build().unwrap();
+
+    let server = server! {
+        request: b"\
+            GET /1 HTTP/1.1\r\n\
+            Host: $HOST\r\n\
+            User-Agent: $USERAGENT\r\n\
+            Accept: */*\r\n\
+            Cookie: a=b; c=d\r\n\
+            Accept-Encoding: gzip\r\n\
+            \r\n\
+            ",
+        response: b"\
+            HTTP/1.1 200 OK\r\n\
+            Server: test\r\n\
+            Content-Length: 0\r\n\
+            \r\n\
+            "
+    };
+
+    let url = format!("http://{}/1", server.addr());
+    let res = client.get(&url).send().unwrap();
+
+    assert_eq!(res.url().as_str(), &url);
+    assert_eq!(res.status(), reqwest::StatusCode::Ok);
+    assert_eq!(res.headers().get(),
+               Some(&reqwest::header::Server::new("test")));
+    assert_eq!(res.headers().get(),
+               Some(&reqwest::header::ContentLength(0)));
+
+    let server = server! {
+        request: b"\
+            GET /2 HTTP/1.1\r\n\
+            Host: $HOST\r\n\
+            User-Agent: $USERAGENT\r\n\
+            Accept: */*\r\n\
+            Cookie: a=b; c=d\r\n\
+            Accept-Encoding: gzip\r\n\
+            \r\n\
+            ",
+        response: b"\
+            HTTP/1.1 200 OK\r\n\
+            Server: test\r\n\
+            Content-Length: 0\r\n\
+            \r\n\
+            "
+    };
+
+    let url = format!("http://{}/2", server.addr());
+    let res = client.get(&url).send().unwrap();
+
+    assert_eq!(res.url().as_str(), &url);
+    assert_eq!(res.status(), reqwest::StatusCode::Ok);
+    assert_eq!(res.headers().get(),
+               Some(&reqwest::header::Server::new("test")));
+    assert_eq!(res.headers().get(),
+               Some(&reqwest::header::ContentLength(0)));
+}
+
+#[test]
+fn test_override_default_headers() {
+    use reqwest::header;
+    let mut headers = header::Headers::with_capacity(1);
+    headers.set(header::Authorization("iamatoken".to_string()));
+    let client = reqwest::Client::builder()
+        .default_headers(headers)
+        .build().unwrap();
+
+    let server = server! {
+        request: b"\
+            GET /3 HTTP/1.1\r\n\
+            Host: $HOST\r\n\
+            User-Agent: $USERAGENT\r\n\
+            Accept: */*\r\n\
+            Authorization: secret\r\n\
+            Accept-Encoding: gzip\r\n\
+            \r\n\
+            ",
+        response: b"\
+            HTTP/1.1 200 OK\r\n\
+            Server: test\r\n\
+            Content-Length: 0\r\n\
+            \r\n\
+            "
+    };
+
+    let url = format!("http://{}/3", server.addr());
+    let res = client.get(&url).header(header::Authorization("secret".to_string())).send().unwrap();
+
+    assert_eq!(res.url().as_str(), &url);
+    assert_eq!(res.status(), reqwest::StatusCode::Ok);
+    assert_eq!(res.headers().get(),
+               Some(&reqwest::header::Server::new("test")));
+    assert_eq!(res.headers().get(),
+               Some(&reqwest::header::ContentLength(0)));
+
+}
diff --git a/tests/gzip.rs b/tests/gzip.rs
index 15715c342..02a3083bc 100644
--- a/tests/gzip.rs
+++ b/tests/gzip.rs
@@ -114,8 +114,8 @@ fn test_accept_header_is_not_changed_if_set() {
         request: b"\
             GET /accept HTTP/1.1\r\n\
             Host: $HOST\r\n\
-            Accept: application/json\r\n\
             User-Agent: $USERAGENT\r\n\
+            Accept: application/json\r\n\
             Accept-Encoding: gzip\r\n\
             \r\n\
             ",
@@ -143,9 +143,9 @@ fn test_accept_encoding_header_is_not_changed_if_set() {
         request: b"\
             GET /accept-encoding HTTP/1.1\r\n\
             Host: $HOST\r\n\
-            Accept-Encoding: identity\r\n\
             User-Agent: $USERAGENT\r\n\
             Accept: */*\r\n\
+            Accept-Encoding: identity\r\n\
             \r\n\
             ",
         response: b"\
diff --git a/tests/multipart.rs b/tests/multipart.rs
index e832b33a5..723df313d 100644
--- a/tests/multipart.rs
+++ b/tests/multipart.rs
@@ -19,10 +19,10 @@ fn test_multipart() {
         request: format!("\
             POST /multipart/1 HTTP/1.1\r\n\
             Host: $HOST\r\n\
-            Content-Type: multipart/form-data; boundary={}\r\n\
-            Content-Length: 123\r\n\
             User-Agent: $USERAGENT\r\n\
             Accept: */*\r\n\
+            Content-Type: multipart/form-data; boundary={}\r\n\
+            Content-Length: 123\r\n\
             Accept-Encoding: gzip\r\n\
             \r\n\
             {}\
diff --git a/tests/redirect.rs b/tests/redirect.rs
index ff46d5a7f..577f3e4e7 100644
--- a/tests/redirect.rs
+++ b/tests/redirect.rs
@@ -117,9 +117,9 @@ fn test_redirect_307_and_308_tries_to_post_again() {
             request: format!("\
                 POST /{} HTTP/1.1\r\n\
                 Host: $HOST\r\n\
-                Content-Length: 5\r\n\
                 User-Agent: $USERAGENT\r\n\
                 Accept: */*\r\n\
+                Content-Length: 5\r\n\
                 Accept-Encoding: gzip\r\n\
                 \r\n\
                 Hello\
@@ -136,9 +136,9 @@ fn test_redirect_307_and_308_tries_to_post_again() {
             request: format!("\
                 POST /dst HTTP/1.1\r\n\
                 Host: $HOST\r\n\
-                Content-Length: 5\r\n\
                 User-Agent: $USERAGENT\r\n\
                 Accept: */*\r\n\
+                Content-Length: 5\r\n\
                 Accept-Encoding: gzip\r\n\
                 Referer: http://$HOST/{}\r\n\
                 \r\n\
@@ -229,9 +229,9 @@ fn test_redirect_removes_sensitive_headers() {
         request: b"\
             GET /sensitive HTTP/1.1\r\n\
             Host: $HOST\r\n\
-            Cookie: foo=bar\r\n\
             User-Agent: $USERAGENT\r\n\
             Accept: */*\r\n\
+            Cookie: foo=bar\r\n\
             Accept-Encoding: gzip\r\n\
             \r\n\
             ",
diff --git a/tests/support/server.rs b/tests/support/server.rs
index 9e1a7f3c9..145bbf0ec 100644
--- a/tests/support/server.rs
+++ b/tests/support/server.rs
@@ -51,7 +51,10 @@ pub fn spawn(txns: Vec<Txn>) -> Server {
 
             let mut n = 0;
             while n < expected.len() {
-                n += socket.read(&mut buf).unwrap();
+                match socket.read(&mut buf[n..]) {
+                    Ok(0) | Err(_) => break,
+                    Ok(nread) => n += nread,
+                }
             }
 
             match (::std::str::from_utf8(&expected), ::std::str::from_utf8(&buf[..n])) {