Repository: algesten/ureq
Pull request #342 (2021-03-14 02:47:20)

Add -k option to cureq example
By analogy with curl, this turns off certificate verification. Requires
enabling the "dangerous_configuration" feature in the rustls dependency.


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 060a33e..b755086 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,23 +26,24 @@ socks-proxy = ["socks"]
 [dependencies]
 base64 = "0.13"
 chunked_transfer = "1.2.0"
-cookie = { version = "0.14", features = ["percent-encode"], optional = true}
+cookie = { version = "0.15", features = ["percent-encode"], optional = true}
 once_cell = "1"
 url = "2"
 socks = { version = "0.3.2", optional = true }
-rustls = { version = "0.19", optional = true, features = [] }
+rustls = { version = "0.19", optional = true }
 webpki = { version = "0.21", optional = true }
 webpki-roots = { version = "0.21", optional = true }
 rustls-native-certs = { version = "0.5", optional = true }
 serde = { version = "1", optional = true }
 serde_json = { version = "1", optional = true }
 encoding_rs = { version = "0.8", optional = true }
-cookie_store = { version = "0.12.0", optional = true }
+cookie_store = { version = "0.13.0", optional = true, default-features = false, features = ["preserve_order"] }
 log = "0.4.11"
 
 [dev-dependencies]
 serde = { version = "1", features = ["derive"] }
 env_logger = "0.8.1"
+rustls = { version = "0.19", features = ["dangerous_configuration"] }
 
 [[example]]
 name = "smoke-test"
diff --git a/examples/cureq/main.rs b/examples/cureq/main.rs
index 8898ca9..183f8b0 100644
--- a/examples/cureq/main.rs
+++ b/examples/cureq/main.rs
@@ -1,10 +1,14 @@
-use std::env;
 use std::error;
 use std::fmt;
 use std::io;
 use std::time::Duration;
+use std::{env, sync::Arc};
 
+use rustls::{
+    Certificate, ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError,
+};
 use ureq;
+use webpki::DNSNameRef;
 
 #[derive(Debug)]
 struct StringError(String);
@@ -79,6 +83,20 @@ fn get(agent: &ureq::Agent, url: &str, print_headers: bool) -> Result<(), Error>
     Ok(())
 }
 
+struct AcceptAll {}
+
+impl ServerCertVerifier for AcceptAll {
+    fn verify_server_cert(
+        &self,
+        _roots: &RootCertStore,
+        _presented_certs: &[Certificate],
+        _dns_name: DNSNameRef<'_>,
+        _ocsp_response: &[u8],
+    ) -> Result<ServerCertVerified, TLSError> {
+        Ok(ServerCertVerified::assertion())
+    }
+}
+
 fn main() {
     match main2() {
         Ok(()) => {}
@@ -103,10 +121,9 @@ Fetch url and copy it to stdout.
     }
     args.remove(0);
     env_logger::init();
-    let agent = ureq::builder()
+    let mut builder = ureq::builder()
         .timeout_connect(Duration::from_secs(30))
-        .timeout(Duration::from_secs(300))
-        .build();
+        .timeout(Duration::from_secs(300));
     let flags: Vec<&String> = args.iter().filter(|s| s.starts_with("-")).collect();
     let nonflags: Vec<&String> = args.iter().filter(|s| !s.starts_with("-")).collect();
 
@@ -114,10 +131,19 @@ Fetch url and copy it to stdout.
     for flag in flags {
         match flag.as_ref() {
             "-i" => print_headers = true,
+            "-k" => {
+                let mut client_config = ClientConfig::new();
+                client_config
+                    .dangerous()
+                    .set_certificate_verifier(Arc::new(AcceptAll {}));
+                builder = builder.tls_config(Arc::new(client_config));
+            }
             f => Err(StringError(format!("unrecognized flag '{}'", f)))?,
         }
     }
 
+    let agent = builder.build();
+
     for url in nonflags {
         get(&agent, &url, print_headers)?;
     }
diff --git a/src/body.rs b/src/body.rs
index 6cb285d..6420bef 100644
--- a/src/body.rs
+++ b/src/body.rs
@@ -150,26 +150,6 @@ fn copy_chunked<R: Read, W: Write>(reader: &mut R, writer: &mut W) -> io::Result
     }
 }
 
-#[test]
-fn test_copy_chunked() {
-    let mut source = Vec::<u8>::new();
-    source.resize(CHUNK_MAX_PAYLOAD_SIZE, 33);
-    source.extend_from_slice(b"hello world");
-
-    let mut dest = Vec::<u8>::new();
-    copy_chunked(&mut &source[..], &mut dest).unwrap();
-
-    let mut dest_expected = Vec::<u8>::new();
-    dest_expected.extend_from_slice(format!("{:x}\r\n", CHUNK_MAX_PAYLOAD_SIZE).as_bytes());
-    dest_expected.resize(dest_expected.len() + CHUNK_MAX_PAYLOAD_SIZE, 33);
-    dest_expected.extend_from_slice(b"\r\n");
-
-    dest_expected.extend_from_slice(b"b\r\nhello world\r\n");
-    dest_expected.extend_from_slice(b"0\r\n\r\n");
-
-    assert_eq!(dest, dest_expected);
-}
-
 /// Helper to send a body, either as chunked or not.
 pub(crate) fn send_body(
     mut body: SizedReader,
@@ -184,3 +164,28 @@ pub(crate) fn send_body(
 
     Ok(())
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_copy_chunked() {
+        let mut source = Vec::<u8>::new();
+        source.resize(CHUNK_MAX_PAYLOAD_SIZE, 33);
+        source.extend_from_slice(b"hello world");
+
+        let mut dest = Vec::<u8>::new();
+        copy_chunked(&mut &source[..], &mut dest).unwrap();
+
+        let mut dest_expected = Vec::<u8>::new();
+        dest_expected.extend_from_slice(format!("{:x}\r\n", CHUNK_MAX_PAYLOAD_SIZE).as_bytes());
+        dest_expected.resize(dest_expected.len() + CHUNK_MAX_PAYLOAD_SIZE, 33);
+        dest_expected.extend_from_slice(b"\r\n");
+
+        dest_expected.extend_from_slice(b"b\r\nhello world\r\n");
+        dest_expected.extend_from_slice(b"0\r\n\r\n");
+
+        assert_eq!(dest, dest_expected);
+    }
+}
diff --git a/src/cookies.rs b/src/cookies.rs
index 2c9d3da..d4e9c1b 100644
--- a/src/cookies.rs
+++ b/src/cookies.rs
@@ -43,6 +43,6 @@ impl CookieTin {
         I: Iterator<Item = cookie::Cookie<'static>>,
     {
         let mut store = self.inner.write().unwrap();
-        store.store_response_cookies(cookies, url)
+        store.store_response_cookies(cookies, url);
     }
 }
diff --git a/src/error.rs b/src/error.rs
index 102bf03..881a04c 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,4 +1,4 @@
-use url::Url;
+use url::{ParseError, Url};
 
 use std::error;
 use std::fmt::{self, Display};
@@ -315,6 +315,14 @@ impl From<Transport> for Error {
     }
 }
 
+impl From<ParseError> for Error {
+    fn from(err: ParseError) -> Self {
+        ErrorKind::InvalidUrl
+            .msg(&format!("failed to parse URL: {:?}", err))
+            .src(err)
+    }
+}
+
 impl fmt::Display for ErrorKind {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
@@ -334,73 +342,76 @@ impl fmt::Display for ErrorKind {
     }
 }
 
-#[test]
-fn status_code_error() {
-    let mut response = Response::new(404, "NotFound", "").unwrap();
-    response.set_url("http://example.org/".parse().unwrap());
-    let err = Error::Status(response.status(), response);
+#[cfg(test)]
+mod tests {
+    use super::*;
 
-    assert_eq!(err.to_string(), "http://example.org/: status code 404");
-}
+    #[test]
+    fn status_code_error() {
+        let mut response = Response::new(404, "NotFound", "").unwrap();
+        response.set_url("http://example.org/".parse().unwrap());
+        let err = Error::Status(response.status(), response);
 
-#[test]
-fn status_code_error_redirect() {
-    use crate::{get, test};
-
-    test::set_handler("/redirect_a", |unit| {
-        assert_eq!(unit.method, "GET");
-        test::make_response(
-            302,
-            "Go here",
-            vec!["Location: test://example.edu/redirect_b"],
-            vec![],
-        )
-    });
-    test::set_handler("/redirect_b", |unit| {
-        assert_eq!(unit.method, "GET");
-        test::make_response(
-            302,
-            "Go here",
-            vec!["Location: http://example.com/status/500"],
-            vec![],
-        )
-    });
-
-    let err = get("test://example.org/redirect_a").call().unwrap_err();
-    assert_eq!(err.kind(), ErrorKind::HTTP, "{:?}", err);
-    assert_eq!(
+        assert_eq!(err.to_string(), "http://example.org/: status code 404");
+    }
+
+    #[test]
+    fn status_code_error_redirect() {
+        use crate::{get, test};
+
+        test::set_handler("/redirect_a", |unit| {
+            assert_eq!(unit.method, "GET");
+            test::make_response(
+                302,
+                "Go here",
+                vec!["Location: test://example.edu/redirect_b"],
+                vec![],
+            )
+        });
+        test::set_handler("/redirect_b", |unit| {
+            assert_eq!(unit.method, "GET");
+            test::make_response(
+                302,
+                "Go here",
+                vec!["Location: http://example.com/status/500"],
+                vec![],
+            )
+        });
+
+        let err = get("test://example.org/redirect_a").call().unwrap_err();
+        assert_eq!(err.kind(), ErrorKind::HTTP, "{:?}", err);
+        assert_eq!(
         err.to_string(),
         "http://example.com/status/500: status code 500 (redirected from test://example.org/redirect_a)"
     );
-}
+    }
 
-#[test]
-fn io_error() {
-    let ioe = io::Error::new(io::ErrorKind::TimedOut, "too slow");
-    let mut err = Error::new(ErrorKind::Io, Some("oops".to_string())).src(ioe);
+    #[test]
+    fn io_error() {
+        let ioe = io::Error::new(io::ErrorKind::TimedOut, "too slow");
+        let mut err = Error::new(ErrorKind::Io, Some("oops".to_string())).src(ioe);
 
-    err = err.url("http://example.com/".parse().unwrap());
-    assert_eq!(
-        err.to_string(),
-        "http://example.com/: Network Error: oops: too slow"
-    );
-}
+        err = err.url("http://example.com/".parse().unwrap());
+        assert_eq!(
+            err.to_string(),
+            "http://example.com/: Network Error: oops: too slow"
+        );
+    }
 
-#[test]
-fn connection_closed() {
-    let ioe = io::Error::new(io::ErrorKind::ConnectionReset, "connection reset");
-    let err = ErrorKind::Io.new().src(ioe);
-    assert!(err.connection_closed());
+    #[test]
+    fn connection_closed() {
+        let ioe = io::Error::new(io::ErrorKind::ConnectionReset, "connection reset");
+        let err = ErrorKind::Io.new().src(ioe);
+        assert!(err.connection_closed());
 
-    let ioe = io::Error::new(io::ErrorKind::ConnectionAborted, "connection aborted");
-    let err = ErrorKind::Io.new().src(ioe);
-    assert!(err.connection_closed());
-}
+        let ioe = io::Error::new(io::ErrorKind::ConnectionAborted, "connection aborted");
+        let err = ErrorKind::Io.new().src(ioe);
+        assert!(err.connection_closed());
+    }
 
-#[test]
-fn error_is_send_and_sync() {
-    fn takes_send(_: impl Send) {}
-    fn takes_sync(_: impl Sync) {}
-    takes_send(crate::error::ErrorKind::InvalidUrl.new());
-    takes_sync(crate::error::ErrorKind::InvalidUrl.new());
+    #[test]
+    fn error_implements_send_and_sync() {
+        let _error: Box<dyn Send> = Box::new(Error::new(ErrorKind::Io, None));
+        let _error: Box<dyn Sync> = Box::new(Error::new(ErrorKind::Io, None));
+    }
 }
diff --git a/src/header.rs b/src/header.rs
index 74cdde1..342f1c1 100644
--- a/src/header.rs
+++ b/src/header.rs
@@ -1,6 +1,66 @@
 use crate::error::{Error, ErrorKind};
 use std::fmt;
-use std::str::FromStr;
+use std::str::{from_utf8, FromStr};
+
+/// Since a status line or header can contain non-utf8 characters the
+/// backing store is a `Vec<u8>`
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) struct HeaderLine(Vec<u8>);
+
+impl From<String> for HeaderLine {
+    fn from(s: String) -> Self {
+        HeaderLine(s.into_bytes())
+    }
+}
+
+impl From<Vec<u8>> for HeaderLine {
+    fn from(b: Vec<u8>) -> Self {
+        HeaderLine(b)
+    }
+}
+
+impl HeaderLine {
+    pub fn into_string_lossy(self) -> String {
+        // Try to avoid an extra allcation.
+        String::from_utf8(self.0)
+            .unwrap_or_else(|e| String::from_utf8_lossy(&e.into_bytes()).to_string())
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+
+    fn as_bytes(&self) -> &[u8] {
+        &self.0
+    }
+
+    pub fn into_header(self) -> Result<Header, Error> {
+        // The header name should always be ascii, we can read anything up to the
+        // ':' delimiter byte-by-byte.
+        let mut index = 0;
+
+        for c in self.as_bytes() {
+            if *c == b':' {
+                break;
+            }
+            if !is_tchar(c) {
+                return Err(Error::new(
+                    ErrorKind::BadHeader,
+                    Some(format!("Invalid char ({:0x?}) while looking for ':'", *c)),
+                ));
+            }
+            index += 1;
+        }
+
+        Ok(Header { line: self, index })
+    }
+}
+
+impl fmt::Display for HeaderLine {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", String::from_utf8_lossy(&self.0))
+    }
+}
 
 #[derive(Clone, PartialEq)]
 /// Wrapper type for a header field.
@@ -8,7 +68,7 @@ use std::str::FromStr;
 pub struct Header {
     // Line contains the unmodified bytes of single header field.
     // It does not contain the final CRLF.
-    line: String,
+    line: HeaderLine,
     // Index is the position of the colon within the header field.
     // Invariant: index > 0
     // Invariant: index + 1 < line.len()
@@ -23,19 +83,53 @@ impl fmt::Debug for Header {
 
 impl Header {
     pub fn new(name: &str, value: &str) -> Self {
-        let line = format!("{}: {}", name, value);
+        let line = format!("{}: {}", name, value).into();
         let index = name.len();
         Header { line, index }
     }
 
     /// The header name.
     pub fn name(&self) -> &str {
-        &self.line.as_str()[0..self.index]
+        let bytes = &self.line.as_bytes()[0..self.index];
+        // Since we validate the header name in HeaderLine::into_header, we
+        // are guaranteed it is valid utf-8 at this point.
+        from_utf8(bytes).expect("Legal chars in header name")
     }
 
     /// The header value.
-    pub fn value(&self) -> &str {
-        &self.line.as_str()[self.index + 1..].trim()
+    ///
+    /// For non-utf8 headers this returns None (use [`Header::value_raw()`]).
+    pub fn value(&self) -> Option<&str> {
+        let bytes = &self.line.as_bytes()[self.index + 1..];
+        from_utf8(bytes)
+            .map(|s| s.trim())
+            .ok()
+            // ensure all bytes are valid field name.
+            .filter(|s| s.as_bytes().iter().all(is_field_vchar_or_obs_fold))
+    }
+
+    /// The header value as a byte slice.
+    ///
+    /// For legacy reasons, the HTTP spec allows headers to be non-ascii characters.
+    /// Typically such headers are encoded in a non-utf8 encoding (such as iso-8859-1).
+    ///
+    /// ureq can't know what encoding the header is in, but this function provides
+    /// an escape hatch for users that need to handle such headers.
+    pub fn value_raw(&self) -> &[u8] {
+        let mut bytes = &self.line.as_bytes()[self.index + 1..];
+
+        if !bytes.is_empty() {
+            // trim front
+            while !bytes.is_empty() && bytes[0].is_ascii_whitespace() {
+                bytes = &bytes[1..];
+            }
+            // trim back
+            while !bytes.is_empty() && bytes[bytes.len() - 1].is_ascii_whitespace() {
+                bytes = &bytes[..(bytes.len() - 1)];
+            }
+        }
+
+        bytes
     }
 
     /// Compares the given str to the header name ignoring case.
@@ -44,7 +138,11 @@ impl Header {
     }
 
     pub(crate) fn validate(&self) -> Result<(), Error> {
-        if !valid_name(self.name()) || !valid_value(&self.line.as_str()[self.index + 1..]) {
+        let bytes = self.line.as_bytes();
+        let name_raw = &bytes[0..self.index];
+        let value_raw = &bytes[self.index + 1..];
+
+        if !valid_name(name_raw) || !valid_value(value_raw) {
             Err(ErrorKind::BadHeader.msg(&format!("invalid header '{}'", self.line)))
         } else {
             Ok(())
@@ -53,14 +151,17 @@ impl Header {
 }
 
 pub fn get_header<'a, 'b>(headers: &'b [Header], name: &'a str) -> Option<&'b str> {
-    headers.iter().find(|h| h.is_name(name)).map(|h| h.value())
+    headers
+        .iter()
+        .find(|h| h.is_name(name))
+        .and_then(|h| h.value())
 }
 
 pub fn get_all_headers<'a, 'b>(headers: &'b [Header], name: &'a str) -> Vec<&'b str> {
     headers
         .iter()
         .filter(|h| h.is_name(name))
-        .map(|h| h.value())
+        .filter_map(|h| h.value())
         .collect()
 }
 
@@ -84,12 +185,12 @@ pub fn add_header(headers: &mut Vec<Header>, header: Header) {
 // token = 1*tchar
 // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
 // "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
-fn valid_name(name: &str) -> bool {
-    !name.is_empty() && name.bytes().all(is_tchar)
+fn valid_name(name: &[u8]) -> bool {
+    !name.is_empty() && name.iter().all(is_tchar)
 }
 
 #[inline]
-fn is_tchar(b: u8) -> bool {
+fn is_tchar(b: &u8) -> bool {
     match b {
         b'!' | b'#' | b'$' | b'%' | b'&' => true,
         b'\'' | b'*' | b'+' | b'-' | b'.' => true,
@@ -112,12 +213,12 @@ fn is_tchar(b: u8) -> bool {
 // https://tools.ietf.org/html/rfc5234#appendix-B.1
 // VCHAR          =  %x21-7E
 //                        ; visible (printing) characters
-fn valid_value(value: &str) -> bool {
-    value.bytes().all(is_field_vchar_or_obs_fold)
+fn valid_value(value: &[u8]) -> bool {
+    value.iter().all(is_field_vchar_or_obs_fold)
 }
 
 #[inline]
-fn is_field_vchar_or_obs_fold(b: u8) -> bool {
+fn is_field_vchar_or_obs_fold(b: &u8) -> bool {
     match b {
         b' ' | b'\t' => true,
         0x21..=0x7E => true,
@@ -129,88 +230,107 @@ impl FromStr for Header {
     type Err = Error;
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         //
-        let line = s.to_string();
-        let index = s
-            .find(':')
-            .ok_or_else(|| ErrorKind::BadHeader.msg("no colon in header"))?;
-
-        // no value?
-        if index >= s.len() {
-            return Err(ErrorKind::BadHeader.msg("no value in header"));
-        }
+        let line: HeaderLine = s.to_string().into();
+
+        let header = line.into_header()?;
 
-        let header = Header { line, index };
         header.validate()?;
         Ok(header)
     }
 }
 
-#[test]
-fn test_valid_name() {
-    assert!(valid_name("example"));
-    assert!(valid_name("Content-Type"));
-    assert!(valid_name("h-123456789"));
-    assert!(!valid_name("Content-Type:"));
-    assert!(!valid_name("Content-Type "));
-    assert!(!valid_name(" some-header"));
-    assert!(!valid_name("\"invalid\""));
-    assert!(!valid_name("Gödel"));
-}
-
-#[test]
-fn test_valid_value() {
-    assert!(valid_value("example"));
-    assert!(valid_value("foo bar"));
-    assert!(valid_value(" foobar "));
-    assert!(valid_value(" foo\tbar "));
-    assert!(valid_value(" foo~"));
-    assert!(valid_value(" !bar"));
-    assert!(valid_value(" "));
-    assert!(!valid_value(" \nfoo"));
-    assert!(!valid_value("foo\x7F"));
-}
-
-#[test]
-fn test_parse_invalid_name() {
-    let cases = vec![
-        "Content-Type  :",
-        " Content-Type: foo",
-        "Content-Type foo",
-        "\"some-header\": foo",
-        "Gödel: Escher, Bach",
-        "Foo: \n",
-        "Foo: \nbar",
-        "Foo: \x7F bar",
-    ];
-    for c in cases {
-        let result = c.parse::<Header>();
-        assert!(
-            matches!(result, Err(ref e) if e.kind() == ErrorKind::BadHeader),
-            "'{}'.parse(): expected BadHeader, got {:?}",
-            c,
-            result
-        );
-    }
-}
-
-#[test]
-fn empty_value() {
-    let h = "foo:".parse::<Header>().unwrap();
-    assert_eq!(h.value(), "");
-}
-
-#[test]
-fn value_with_whitespace() {
-    let h = "foo:      bar    ".parse::<Header>().unwrap();
-    assert_eq!(h.value(), "bar");
-}
-
-#[test]
-fn name_and_value() {
-    let header: Header = "X-Forwarded-For: 127.0.0.1".parse().unwrap();
-    assert_eq!("X-Forwarded-For", header.name());
-    assert_eq!("127.0.0.1", header.value());
-    assert!(header.is_name("X-Forwarded-For"));
-    assert!(header.is_name("x-forwarded-for"));
-    assert!(header.is_name("X-FORWARDED-FOR"));
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_valid_name() {
+        assert!(valid_name(b"example"));
+        assert!(valid_name(b"Content-Type"));
+        assert!(valid_name(b"h-123456789"));
+        assert!(!valid_name(b"Content-Type:"));
+        assert!(!valid_name(b"Content-Type "));
+        assert!(!valid_name(b" some-header"));
+        assert!(!valid_name(b"\"invalid\""));
+        assert!(!valid_name(b"G\xf6del"));
+    }
+
+    #[test]
+    fn test_valid_value() {
+        assert!(valid_value(b"example"));
+        assert!(valid_value(b"foo bar"));
+        assert!(valid_value(b" foobar "));
+        assert!(valid_value(b" foo\tbar "));
+        assert!(valid_value(b" foo~"));
+        assert!(valid_value(b" !bar"));
+        assert!(valid_value(b" "));
+        assert!(!valid_value(b" \nfoo"));
+        assert!(!valid_value(b"foo\x7F"));
+    }
+
+    #[test]
+    fn test_parse_invalid_name() {
+        let cases = vec![
+            "Content-Type  :",
+            " Content-Type: foo",
+            "Content-Type foo",
+            "\"some-header\": foo",
+            "Gödel: Escher, Bach",
+            "Foo: \n",
+            "Foo: \nbar",
+            "Foo: \x7F bar",
+        ];
+        for c in cases {
+            let result = c.parse::<Header>();
+            assert!(
+                matches!(result, Err(ref e) if e.kind() == ErrorKind::BadHeader),
+                "'{}'.parse(): expected BadHeader, got {:?}",
+                c,
+                result
+            );
+        }
+    }
+
+    #[test]
+    #[cfg(feature = "charset")]
+    fn test_parse_non_utf8_value() {
+        let (cow, _, _) = encoding_rs::WINDOWS_1252.encode("x-geo-stuff: älvsjö ");
+        let bytes = cow.to_vec();
+        let line: HeaderLine = bytes.into();
+        let header = line.into_header().unwrap();
+        assert_eq!(header.name(), "x-geo-stuff");
+        assert_eq!(header.value(), None);
+        assert_eq!(header.value_raw(), [228, 108, 118, 115, 106, 246]);
+    }
+
+    #[test]
+    fn empty_value() {
+        let h = "foo:".parse::<Header>().unwrap();
+        assert_eq!(h.value(), Some(""));
+    }
+
+    #[test]
+    fn value_with_whitespace() {
+        let h = "foo:      bar    ".parse::<Header>().unwrap();
+        assert_eq!(h.value(), Some("bar"));
+    }
+
+    #[test]
+    fn name_and_value() {
+        let header: Header = "X-Forwarded-For: 127.0.0.1".parse().unwrap();
+        assert_eq!("X-Forwarded-For", header.name());
+        assert_eq!(header.value(), Some("127.0.0.1"));
+        assert!(header.is_name("X-Forwarded-For"));
+        assert!(header.is_name("x-forwarded-for"));
+        assert!(header.is_name("X-FORWARDED-FOR"));
+    }
+
+    #[test]
+    fn test_iso8859_utf8_mixup() {
+        // C2 A5 is ¥ in UTF-8 and Â¥ in ISO-8859-1
+        let b = "header: \0xc2\0xa5".to_string().into_bytes();
+        let l: HeaderLine = b.into();
+        let h = l.into_header().unwrap();
+        assert_eq!(h.value(), None);
+    }
 }
diff --git a/src/pool.rs b/src/pool.rs
index bc20475..26ad5a1 100644
--- a/src/pool.rs
+++ b/src/pool.rs
@@ -219,88 +219,6 @@ impl PoolKey {
     }
 }
 
-#[test]
-fn poolkey_new() {
-    // Test that PoolKey::new() does not panic on unrecognized schemes.
-    PoolKey::new(&Url::parse("zzz:///example.com").unwrap(), None);
-}
-
-#[test]
-fn pool_connections_limit() {
-    // Test inserting connections with different keys into the pool,
-    // filling and draining it. The pool should evict earlier connections
-    // when the connection limit is reached.
-    let pool = ConnectionPool::new_with_limits(10, 1);
-    let hostnames = (0..pool.max_idle_connections * 2).map(|i| format!("{}.example", i));
-    let poolkeys = hostnames.map(|hostname| PoolKey {
-        scheme: "https".to_string(),
-        hostname,
-        port: Some(999),
-        proxy: None,
-    });
-    for key in poolkeys.clone() {
-        pool.add(key, Stream::from_vec(vec![]))
-    }
-    assert_eq!(pool.len(), pool.max_idle_connections);
-
-    for key in poolkeys.skip(pool.max_idle_connections) {
-        let result = pool.remove(&key);
-        assert!(result.is_some(), "expected key was not in pool");
-    }
-    assert_eq!(pool.len(), 0)
-}
-
-#[test]
-fn pool_per_host_connections_limit() {
-    // Test inserting connections with the same key into the pool,
-    // filling and draining it. The pool should evict earlier connections
-    // when the per-host connection limit is reached.
-    let pool = ConnectionPool::new_with_limits(10, 2);
-    let poolkey = PoolKey {
-        scheme: "https".to_string(),
-        hostname: "example.com".to_string(),
-        port: Some(999),
-        proxy: None,
-    };
-
-    for _ in 0..pool.max_idle_connections_per_host * 2 {
-        pool.add(poolkey.clone(), Stream::from_vec(vec![]))
-    }
-    assert_eq!(pool.len(), pool.max_idle_connections_per_host);
-
-    for _ in 0..pool.max_idle_connections_per_host {
-        let result = pool.remove(&poolkey);
-        assert!(result.is_some(), "expected key was not in pool");
-    }
-    assert_eq!(pool.len(), 0);
-}
-
-#[test]
-fn pool_checks_proxy() {
-    // Test inserting different poolkeys with same address but different proxies.
-    // Each insertion should result in an additional entry in the pool.
-    let pool = ConnectionPool::new_with_limits(10, 1);
-    let url = Url::parse("zzz:///example.com").unwrap();
-
-    pool.add(PoolKey::new(&url, None), Stream::from_vec(vec![]));
-    assert_eq!(pool.len(), 1);
-
-    pool.add(
-        PoolKey::new(&url, Some(Proxy::new("localhost:9999").unwrap())),
-        Stream::from_vec(vec![]),
-    );
-    assert_eq!(pool.len(), 2);
-
-    pool.add(
-        PoolKey::new(
-            &url,
-            Some(Proxy::new("user:password@localhost:9999").unwrap()),
-        ),
-        Stream::from_vec(vec![]),
-    );
-    assert_eq!(pool.len(), 3);
-}
-
 /// Read wrapper that returns the stream to the pool once the
 /// read is exhausted (reached a 0).
 ///
@@ -360,3 +278,90 @@ impl<R: Read + Sized + Into<Stream>> Read for PoolReturnRead<R> {
         Ok(amount)
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn poolkey_new() {
+        // Test that PoolKey::new() does not panic on unrecognized schemes.
+        PoolKey::new(&Url::parse("zzz:///example.com").unwrap(), None);
+    }
+
+    #[test]
+    fn pool_connections_limit() {
+        // Test inserting connections with different keys into the pool,
+        // filling and draining it. The pool should evict earlier connections
+        // when the connection limit is reached.
+        let pool = ConnectionPool::new_with_limits(10, 1);
+        let hostnames = (0..pool.max_idle_connections * 2).map(|i| format!("{}.example", i));
+        let poolkeys = hostnames.map(|hostname| PoolKey {
+            scheme: "https".to_string(),
+            hostname,
+            port: Some(999),
+            proxy: None,
+        });
+        for key in poolkeys.clone() {
+            pool.add(key, Stream::from_vec(vec![]))
+        }
+        assert_eq!(pool.len(), pool.max_idle_connections);
+
+        for key in poolkeys.skip(pool.max_idle_connections) {
+            let result = pool.remove(&key);
+            assert!(result.is_some(), "expected key was not in pool");
+        }
+        assert_eq!(pool.len(), 0)
+    }
+
+    #[test]
+    fn pool_per_host_connections_limit() {
+        // Test inserting connections with the same key into the pool,
+        // filling and draining it. The pool should evict earlier connections
+        // when the per-host connection limit is reached.
+        let pool = ConnectionPool::new_with_limits(10, 2);
+        let poolkey = PoolKey {
+            scheme: "https".to_string(),
+            hostname: "example.com".to_string(),
+            port: Some(999),
+            proxy: None,
+        };
+
+        for _ in 0..pool.max_idle_connections_per_host * 2 {
+            pool.add(poolkey.clone(), Stream::from_vec(vec![]))
+        }
+        assert_eq!(pool.len(), pool.max_idle_connections_per_host);
+
+        for _ in 0..pool.max_idle_connections_per_host {
+            let result = pool.remove(&poolkey);
+            assert!(result.is_some(), "expected key was not in pool");
+        }
+        assert_eq!(pool.len(), 0);
+    }
+
+    #[test]
+    fn pool_checks_proxy() {
+        // Test inserting different poolkeys with same address but different proxies.
+        // Each insertion should result in an additional entry in the pool.
+        let pool = ConnectionPool::new_with_limits(10, 1);
+        let url = Url::parse("zzz:///example.com").unwrap();
+
+        pool.add(PoolKey::new(&url, None), Stream::from_vec(vec![]));
+        assert_eq!(pool.len(), 1);
+
+        pool.add(
+            PoolKey::new(&url, Some(Proxy::new("localhost:9999").unwrap())),
+            Stream::from_vec(vec![]),
+        );
+        assert_eq!(pool.len(), 2);
+
+        pool.add(
+            PoolKey::new(
+                &url,
+                Some(Proxy::new("user:password@localhost:9999").unwrap()),
+            ),
+            Stream::from_vec(vec![]),
+        );
+        assert_eq!(pool.len(), 3);
+    }
+}
diff --git a/src/proxy.rs b/src/proxy.rs
index 0daea67..0aed467 100644
--- a/src/proxy.rs
+++ b/src/proxy.rs
@@ -171,8 +171,7 @@ Proxy-Connection: Keep-Alive\r\n\
 
 #[cfg(test)]
 mod tests {
-    use super::Proto;
-    use super::Proxy;
+    use super::*;
 
     #[test]
     fn parse_proxy_fakeproto() {
diff --git a/src/request.rs b/src/request.rs
index 182d4b6..0113990 100644
--- a/src/request.rs
+++ b/src/request.rs
@@ -4,7 +4,6 @@ use std::{fmt, time};
 use url::{form_urlencoded, Url};
 
 use crate::body::Payload;
-use crate::error::ErrorKind;
 use crate::header::{self, Header};
 use crate::unit::{self, Unit};
 use crate::Response;
@@ -15,10 +14,29 @@ use super::SerdeValue;
 
 pub type Result<T> = std::result::Result<T, Error>;
 
-#[derive(Debug, Clone)]
-enum Urlish {
-    Url(Url),
-    Str(String),
+#[derive(Clone)]
+struct ParsedUrl(std::result::Result<Url, url::ParseError>);
+
+impl fmt::Display for ParsedUrl {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if let Ok(url) = &self.0 {
+            write!(f, "{}", url.as_str())
+        } else {
+            write!(f, "{:?}", self.0)
+        }
+    }
+}
+
+impl From<String> for ParsedUrl {
+    fn from(s: String) -> Self {
+        ParsedUrl(s.parse())
+    }
+}
+
+impl From<Url> for ParsedUrl {
+    fn from(url: Url) -> Self {
+        ParsedUrl(Ok(url))
+    }
 }
 
 /// Request instances are builders that creates a request.
@@ -36,53 +54,38 @@ enum Urlish {
 pub struct Request {
     agent: Agent,
     method: String,
-    url: Urlish,
+    parsed_url: ParsedUrl,
     error_on_non_2xx: bool,
     headers: Vec<Header>,
-    query_params: Vec<(String, String)>,
     timeout: Option<time::Duration>,
 }
 
-impl fmt::Display for Urlish {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Urlish::Url(u) => write!(f, "{}", u),
-            Urlish::Str(s) => write!(f, "{}", s),
-        }
-    }
-}
-
 impl fmt::Debug for Request {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(
             f,
-            "Request({} {} {:?}, {:?})",
-            self.method, self.url, self.query_params, self.headers
+            "Request({} {}, {:?})",
+            self.method, self.parsed_url, self.headers
         )
     }
 }
 
 impl Request {
     pub(crate) fn new(agent: Agent, method: String, url: String) -> Request {
-        Request {
-            agent,
-            method,
-            url: Urlish::Str(url),
-            headers: vec![],
-            error_on_non_2xx: true,
-            query_params: vec![],
-            timeout: None,
-        }
+        Self::_new(agent, method, url.into())
     }
 
     pub(crate) fn with_url(agent: Agent, method: String, url: Url) -> Request {
+        Self::_new(agent, method, url.into())
+    }
+
+    fn _new(agent: Agent, method: String, parsed_url: ParsedUrl) -> Request {
         Request {
             agent,
             method,
-            url: Urlish::Url(url),
+            parsed_url,
             headers: vec![],
             error_on_non_2xx: true,
-            query_params: vec![],
             timeout: None,
         }
     }
@@ -111,21 +114,12 @@ impl Request {
         self.do_call(Payload::Empty)
     }
 
-    fn do_call(&self, payload: Payload) -> Result<Response> {
+    fn do_call(self, payload: Payload) -> Result<Response> {
         for h in &self.headers {
             h.validate()?;
         }
-        let mut url: Url = match self.url.clone() {
-            Urlish::Url(u) => u,
-            Urlish::Str(s) => s.parse().map_err(|e: url::ParseError| {
-                ErrorKind::InvalidUrl
-                    .msg(&format!("failed to parse URL '{}'", self.url))
-                    .src(e)
-            })?,
-        };
-        for (name, value) in self.query_params.clone() {
-            url.query_pairs_mut().append_pair(&name, &value);
-        }
+        let url = self.parsed_url.0?;
+
         let deadline = match self.timeout.or(self.agent.config.timeout) {
             None => None,
             Some(timeout) => {
@@ -349,7 +343,7 @@ impl Request {
     /// ```
     /// # fn main() -> Result<(), ureq::Error> {
     /// # ureq::is_test(true);
-    /// let resp = ureq::get("http://httpbin.org/response-headers")
+    /// let resp = ureq::get("http://httpbin.org/get")
     ///     .query("format", "json")
     ///     .query("dest", "/login")
     ///     .call()?;
@@ -357,31 +351,76 @@ impl Request {
     /// # }
     /// ```
     pub fn query(mut self, param: &str, value: &str) -> Self {
-        self.query_params
-            .push((param.to_string(), value.to_string()));
+        if let Ok(url) = &mut self.parsed_url.0 {
+            url.query_pairs_mut().append_pair(param, value);
+        }
         self
     }
-}
 
-#[test]
-fn request_implements_send_and_sync() {
-    let _request: Box<dyn Send> = Box::new(Request::new(
-        Agent::new(),
-        "GET".to_string(),
-        "https://example.com/".to_string(),
-    ));
-    let _request: Box<dyn Sync> = Box::new(Request::new(
-        Agent::new(),
-        "GET".to_string(),
-        "https://example.com/".to_string(),
-    ));
+    /// Returns the value of the request method. Something like `GET`, `POST`, `PUT` etc.
+    ///
+    /// ```
+    /// let req = ureq::put("http://httpbin.org/put");
+    ///
+    /// assert_eq!(req.method(), "PUT");
+    /// ```
+    pub fn method(&self) -> &str {
+        &self.method
+    }
+
+    /// Returns all query parameters as a vector of key-value pairs.
+    ///
+    /// ```
+    /// # fn main() -> Result<(), ureq::Error> {
+    /// # ureq::is_test(true);
+    /// let req = ureq::get("http://httpbin.org/get")
+    ///     .query("foo", "42")
+    ///     .query("foo", "43");
+    ///
+    /// assert_eq!(req.query_params(), vec![
+    ///     ("foo".to_string(), "42".to_string()),
+    ///     ("foo".to_string(), "43".to_string())
+    /// ]);
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn query_params(&self) -> Vec<(String, String)> {
+        let mut ret = vec![];
+
+        if let Ok(url) = &self.parsed_url.0 {
+            for (k, v) in url.query_pairs() {
+                ret.push((k.into(), v.into()));
+            }
+        }
+
+        ret
+    }
 }
 
-#[test]
-fn send_byte_slice() {
-    let bytes = vec![1, 2, 3];
-    crate::agent()
-        .post("http://example.com")
-        .send(&bytes[1..2])
-        .ok();
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn request_implements_send_and_sync() {
+        let _request: Box<dyn Send> = Box::new(Request::new(
+            Agent::new(),
+            "GET".to_string(),
+            "https://example.com/".to_string(),
+        ));
+        let _request: Box<dyn Sync> = Box::new(Request::new(
+            Agent::new(),
+            "GET".to_string(),
+            "https://example.com/".to_string(),
+        ));
+    }
+
+    #[test]
+    fn send_byte_slice() {
+        let bytes = vec![1, 2, 3];
+        crate::agent()
+            .post("http://example.com")
+            .send(&bytes[1..2])
+            .ok();
+    }
 }
diff --git a/src/response.rs b/src/response.rs
index b38a81b..fc6ca2f 100644
--- a/src/response.rs
+++ b/src/response.rs
@@ -6,7 +6,7 @@ use chunked_transfer::Decoder as ChunkDecoder;
 use url::Url;
 
 use crate::error::{Error, ErrorKind::BadStatus};
-use crate::header::Header;
+use crate::header::{get_all_headers, get_header, Header, HeaderLine};
 use crate::pool::PoolReturnRead;
 use crate::stream::{DeadlineStream, Stream};
 use crate::unit::Unit;
@@ -119,20 +119,31 @@ impl Response {
     }
 
     /// The status text: `OK`
+    ///
+    /// The HTTP spec allows for non-utf8 status texts. This uses from_utf8_lossy to
+    /// convert such lines to &str.
     pub fn status_text(&self) -> &str {
         &self.status_line.as_str()[self.index.response_code + 1..].trim()
     }
 
-    /// The header corresponding header value for the give name, if any.
+    /// The header value for the given name, or None if not found.
+    ///
+    /// For historical reasons, the HTTP spec allows for header values
+    /// to be encoded using encodigs like iso-8859-1. Such encodings
+    /// means the values are not possible to interpret as utf-8.
+    ///
+    /// In case the header value can't be read as utf-8, this function
+    /// returns `None` (while the name is visible in [`Response::headers_names()`]).
     pub fn header(&self, name: &str) -> Option<&str> {
-        self.headers
-            .iter()
-            .find(|h| h.is_name(name))
-            .map(|h| h.value())
+        get_header(&self.headers, name)
     }
 
     /// A list of the header names in this response.
     /// Lowercased to be uniform.
+    ///
+    /// It's possible for a header name to be returned by this function, and
+    /// still give a `None` value. See [`Response::header()`] for an explanation
+    /// as to why.
     pub fn headers_names(&self) -> Vec<String> {
         self.headers
             .iter()
@@ -147,11 +158,7 @@ impl Response {
 
     /// All headers corresponding values for the give name, or empty vector.
     pub fn all(&self, name: &str) -> Vec<&str> {
-        self.headers
-            .iter()
-            .filter(|h| h.is_name(name))
-            .map(|h| h.value())
-            .collect()
+        get_all_headers(&self.headers, name)
     }
 
     /// The content type part of the "Content-Type" header without
@@ -414,8 +421,9 @@ impl Response {
         // HTTP/1.1 200 OK\r\n
         let mut stream =
             stream::DeadlineStream::new(stream, unit.as_ref().and_then(|u| u.deadline));
-        let status_line = read_next_line(&mut stream, "the status line")?;
 
+        // The status line we can ignore non-utf8 chars and parse as_str_lossy().
+        let status_line = read_next_line(&mut stream, "the status line")?.into_string_lossy();
         let (index, status) = parse_status_line(status_line.as_str())?;
 
         let mut headers: Vec<Header> = Vec::new();
@@ -424,7 +432,7 @@ impl Response {
             if line.is_empty() {
                 break;
             }
-            if let Ok(header) = line.as_str().parse::<Header>() {
+            if let Ok(header) = line.into_header() {
                 headers.push(header);
             }
         }
@@ -539,22 +547,13 @@ impl FromStr for Response {
     }
 }
 
-fn read_next_line(reader: &mut impl BufRead, context: &str) -> io::Result<String> {
-    let mut s = String::new();
-    let result = reader.read_line(&mut s);
+fn read_next_line(reader: &mut impl BufRead, context: &str) -> io::Result<HeaderLine> {
+    let mut buf = Vec::new();
+    let result = reader.read_until(b'\n', &mut buf);
 
     if let Err(e) = result {
         // Provide context to errors encountered while reading the line.
-        // ureq does not currently handle non-ascii status lines and
-        // header values. For historical reasons, the HTTP spec does
-        // allow for characters in the range 0x80-0xff, but these are
-        // very rarely encountered in the wild.
-        // See https://github.com/algesten/ureq/issues/320
-        let reason = if e.kind() == io::ErrorKind::InvalidData {
-            format!("Invalid data in {}", context)
-        } else {
-            format!("Error encountered in {}", context)
-        };
+        let reason = format!("Error encountered in {}", context);
 
         let kind = e.kind();
 
@@ -572,17 +571,19 @@ fn read_next_line(reader: &mut impl BufRead, context: &str) -> io::Result<String
         ));
     }
 
-    if !s.ends_with("\n") {
+    if !buf.ends_with(b"\n") {
         return Err(io::Error::new(
             io::ErrorKind::InvalidInput,
-            format!("Header field didn't end with \\n: {}", s),
+            format!("Header field didn't end with \\n: {:?}", buf),
         ));
     }
-    s.pop();
-    if s.ends_with("\r") {
-        s.pop();
+
+    buf.pop();
+    if buf.ends_with(b"\r") {
+        buf.pop();
     }
-    Ok(s)
+
+    Ok(buf.into())
 }
 
 /// Limits a `Read` to a content size (as set by a "Content-Length" header).
@@ -632,15 +633,6 @@ impl<R: Read> Read for LimitedRead<R> {
     }
 }
 
-#[test]
-fn short_read() {
-    use std::io::Cursor;
-    let mut lr = LimitedRead::new(Cursor::new(vec![b'a'; 3]), 10);
-    let mut buf = vec![0; 1000];
-    let result = lr.read_to_end(&mut buf);
-    assert!(result.err().unwrap().kind() == io::ErrorKind::UnexpectedEof);
-}
-
 impl<R: Read> From<LimitedRead<R>> for Stream
 where
     Stream: From<R>,
@@ -667,10 +659,30 @@ pub(crate) fn charset_from_content_type(header: Option<&str>) -> &str {
         .unwrap_or(DEFAULT_CHARACTER_SET)
 }
 
+// ErrorReader returns an error for every read.
+// The error is as close to a clone of the underlying
+// io::Error as we can get.
+struct ErrorReader(io::Error);
+
+impl Read for ErrorReader {
+    fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
+        Err(io::Error::new(self.0.kind(), self.0.to_string()))
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
 
+    #[test]
+    fn short_read() {
+        use std::io::Cursor;
+        let mut lr = LimitedRead::new(Cursor::new(vec![b'a'; 3]), 10);
+        let mut buf = vec![0; 1000];
+        let result = lr.read_to_end(&mut buf);
+        assert!(result.err().unwrap().kind() == io::ErrorKind::UnexpectedEof);
+    }
+
     #[test]
     fn content_type_without_charset() {
         let s = "HTTP/1.1 200 OK\r\n\
@@ -797,12 +809,24 @@ mod tests {
         let bytes = cow.to_vec();
         let mut reader = io::BufReader::new(io::Cursor::new(bytes));
         let r = read_next_line(&mut reader, "test status line");
-        let e = r.unwrap_err();
-        assert_eq!(e.kind(), io::ErrorKind::InvalidData);
-        assert_eq!(
-            e.to_string(),
-            "Network Error: Invalid data in test status line: stream did not contain valid UTF-8"
+        let h = r.unwrap();
+        assert_eq!(h.to_string(), "HTTP/1.1 302 D�plac� Temporairement");
+    }
+
+    #[test]
+    #[cfg(feature = "charset")]
+    fn parse_header_with_non_utf8() {
+        let (cow, _, _) = encoding_rs::WINDOWS_1252.encode(
+            "HTTP/1.1 200 OK\r\n\
+            x-geo-header: gött mos!\r\n\
+            \r\n\
+            OK",
         );
+        let v = cow.to_vec();
+        let s = Stream::from_vec(v);
+        let resp = Response::do_from_stream(s.into(), None).unwrap();
+        assert_eq!(resp.status(), 200);
+        assert_eq!(resp.header("x-geo-header"), None);
     }
 
     #[test]
@@ -822,15 +846,10 @@ mod tests {
         let hist: Vec<&str> = response2.history.iter().map(|r| &**r).collect();
         assert_eq!(hist, ["http://1.example.com/", "http://2.example.com/"])
     }
-}
-
-// ErrorReader returns an error for every read.
-// The error is as close to a clone of the underlying
-// io::Error as we can get.
-struct ErrorReader(io::Error);
 
-impl Read for ErrorReader {
-    fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
-        Err(io::Error::new(self.0.kind(), self.0.to_string()))
+    #[test]
+    fn response_implements_send_and_sync() {
+        let _response: Box<dyn Send> = Box::new(Response::new(302, "Found", "").unwrap());
+        let _response: Box<dyn Sync> = Box::new(Response::new(302, "Found", "").unwrap());
     }
 }
diff --git a/src/stream.rs b/src/stream.rs
index e7aa813..84d3933 100644
--- a/src/stream.rs
+++ b/src/stream.rs
@@ -353,11 +353,8 @@ pub(crate) fn connect_https(unit: &Unit, hostname: &str) -> Result<Stream, Error
     let mut sock = connect_host(unit, hostname, port)?;
     let mut sess = rustls::ClientSession::new(&tls_conf, sni);
 
-    sess.complete_io(&mut sock).map_err(|err| {
-        ErrorKind::ConnectionFailed
-            .msg("error during TLS handshake. Sometimes this means the host doesn't support any of the same ciphersuites as rustls, or doesn't support TLS 1.2 and above")
-            .src(err)
-    })?;
+    sess.complete_io(&mut sock)
+        .map_err(|err| ErrorKind::ConnectionFailed.new().src(err))?;
     let stream = rustls::StreamOwned::new(sess, sock);
 
     Ok(Stream::from_tls_stream(stream))
diff --git a/src/unit.rs b/src/unit.rs
index 601c90a..99f63c7 100644
--- a/src/unit.rs
+++ b/src/unit.rs
@@ -386,7 +386,9 @@ fn send_prelude(unit: &Unit, stream: &mut Stream, redir: bool) -> io::Result<()>
     // other headers
     for header in &unit.headers {
         if !redir || !header.is_name("Authorization") {
-            write!(prelude, "{}: {}\r\n", header.name(), header.value())?;
+            if let Some(v) = header.value() {
+                write!(prelude, "{}: {}\r\n", header.name(), v)?;
+            }
         }
     }
 


Test patch:
diff --git a/src/test/simple.rs b/src/test/simple.rs
index 842bc37..782f947 100644
--- a/src/test/simple.rs
+++ b/src/test/simple.rs
@@ -136,7 +136,7 @@ fn request_debug() {
 
     assert_eq!(
         s,
-        "Request(GET http://localhost/my/page [], [Authorization: abcdef, \
+        "Request(GET http://localhost/my/page, [Authorization: abcdef, \
          Content-Length: 1234, Content-Type: application/json])"
     );
 
@@ -148,7 +148,7 @@ fn request_debug() {
 
     assert_eq!(
         s,
-        "Request(GET http://localhost/my/page?q=z [(\"foo\", \"bar baz\")], [Authorization: abcdef])"
+        "Request(GET http://localhost/my/page?q=z&foo=bar+baz, [Authorization: abcdef])"
     );
 }