From 1bd67668215ff6e498a46d4f1a03b660eee6e838 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 15 Jul 2019 11:15:33 -0700 Subject: [PATCH 01/16] docs(upgrade): update 0.12.x links of examples --- src/client/mod.rs | 2 +- src/upgrade.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 3ad159b278..a109b0b8ef 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -24,7 +24,7 @@ //! ## Example //! //! For a small example program simply fetching a URL, take a look at the -//! [full client example](https://github.com/hyperium/hyper/blob/master/examples/client.rs). +//! [full client example](https://github.com/hyperium/hyper/blob/0.12.x/examples/client.rs). //! //! ``` //! extern crate hyper; diff --git a/src/upgrade.rs b/src/upgrade.rs index 63646fb28c..89e0fb2e83 100644 --- a/src/upgrade.rs +++ b/src/upgrade.rs @@ -3,7 +3,7 @@ //! See [this example][example] showing how upgrades work with both //! Clients and Servers. //! -//! [example]: https://github.com/hyperium/hyper/blob/master/examples/upgrades.rs +//! [example]: https://github.com/hyperium/hyper/blob/0.12.x/examples/upgrades.rs use std::any::TypeId; use std::error::Error as StdError; From e63368b39091b7d398ec6da55c8837114edb6f70 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 15 Jul 2019 11:16:39 -0700 Subject: [PATCH 02/16] v0.12.33 --- Cargo.toml | 2 +- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 52d0b212dc..e500d135a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hyper" -version = "0.12.32" # don't forget to update html_root_url +version = "0.12.33" # don't forget to update html_root_url description = "A fast and correct HTTP library." readme = "README.md" homepage = "https://hyper.rs/" diff --git a/src/lib.rs b/src/lib.rs index f987fb5e33..aef1e45c09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![doc(html_root_url = "https://docs.rs/hyper/0.12.32")] +#![doc(html_root_url = "https://docs.rs/hyper/0.12.33")] #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![cfg_attr(test, deny(warnings))] From a1609fbb332fccffbeb85d16f1cc0bf98c6ede21 Mon Sep 17 00:00:00 2001 From: Gabriel Ganne Date: Thu, 22 Aug 2019 18:28:54 +0200 Subject: [PATCH 03/16] chore(dependencies): update spmc dev dependency to 0.3 (#1913) spmc 0.2.3 has been yanked on 2019-08-15 Passing to 0.2 requires a small change in the tests. Changing the following (as was done in master branch) &'a spmc::Sender -> &'a Mutex> This fixes the cargo issue telling us that he cannot handle smpc: error: failed to select a version for the requirement `spmc = "^0.2"` Signed-off-by: Gabriel Ganne --- Cargo.toml | 2 +- tests/server.rs | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e500d135a5..45918ecfe8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ rustc_version = "0.2" futures-timer = "0.1" num_cpus = "1.0" pretty_env_logger = "0.3" -spmc = "0.2" +spmc = "0.3" url = "1.0" tokio-fs = "0.1" tokio-mockstream = "1.1.0" diff --git a/tests/server.rs b/tests/server.rs index b695c41bd2..6dc572d6d3 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -16,7 +16,7 @@ use std::net::{TcpStream, Shutdown, SocketAddr}; use std::io::{self, Read, Write}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc; -use std::sync::{Arc}; +use std::sync::{Arc, Mutex}; use std::net::{TcpListener as StdTcpListener}; use std::thread; use std::time::Duration; @@ -1739,7 +1739,7 @@ fn skips_content_length_and_body_for_304_responses() { struct Serve { addr: SocketAddr, msg_rx: mpsc::Receiver, - reply_tx: spmc::Sender, + reply_tx: Mutex>, shutdown_signal: Option>, thread: Option>, } @@ -1772,7 +1772,7 @@ impl Serve { Ok(buf) } - fn reply(&self) -> ReplyBuilder { + fn reply(&self) -> ReplyBuilder<'_> { ReplyBuilder { tx: &self.reply_tx } @@ -1782,43 +1782,45 @@ impl Serve { type BoxError = Box; struct ReplyBuilder<'a> { - tx: &'a spmc::Sender, + tx: &'a Mutex>, } impl<'a> ReplyBuilder<'a> { fn status(self, status: hyper::StatusCode) -> Self { - self.tx.send(Reply::Status(status)).unwrap(); + self.tx.lock().unwrap().send(Reply::Status(status)).unwrap(); self } fn version(self, version: hyper::Version) -> Self { - self.tx.send(Reply::Version(version)).unwrap(); + self.tx.lock().unwrap().send(Reply::Version(version)).unwrap(); self } fn header>(self, name: &str, value: V) -> Self { let name = HeaderName::from_bytes(name.as_bytes()).expect("header name"); let value = HeaderValue::from_str(value.as_ref()).expect("header value"); - self.tx.send(Reply::Header(name, value)).unwrap(); + self.tx.lock().unwrap().send(Reply::Header(name, value)).unwrap(); self } fn body>(self, body: T) { - self.tx.send(Reply::Body(body.as_ref().to_vec().into())).unwrap(); + self.tx.lock().unwrap().send(Reply::Body(body.as_ref().to_vec().into())).unwrap(); } fn body_stream(self, body: Body) { - self.tx.send(Reply::Body(body)).unwrap(); + self.tx.lock().unwrap().send(Reply::Body(body)).unwrap(); } fn error>(self, err: E) { - self.tx.send(Reply::Error(err.into())).unwrap(); + self.tx.lock().unwrap().send(Reply::Error(err.into())).unwrap(); } } impl<'a> Drop for ReplyBuilder<'a> { fn drop(&mut self) { - let _ = self.tx.send(Reply::End); + if let Ok(mut tx) = self.tx.lock() { + let _ = tx.send(Reply::End); + } } } @@ -2006,7 +2008,7 @@ impl ServeOptions { Serve { msg_rx: msg_rx, - reply_tx: reply_tx, + reply_tx: Mutex::new(reply_tx), addr: addr, shutdown_signal: Some(shutdown_tx), thread: Some(thread), From 23fc8b0806e7fde435ca00479cd5e3c8c5bdeee7 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Wed, 4 Sep 2019 14:32:06 -0700 Subject: [PATCH 04/16] fix(client): allow client GET requests with explicit body headers Closes #1925 --- src/proto/h1/role.rs | 91 +++++++++++----------- tests/client.rs | 175 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 221 insertions(+), 45 deletions(-) diff --git a/src/proto/h1/role.rs b/src/proto/h1/role.rs index e4cb11545b..902f93beaf 100644 --- a/src/proto/h1/role.rs +++ b/src/proto/h1/role.rs @@ -781,31 +781,44 @@ impl Client { impl Client { fn set_length(head: &mut RequestHead, body: Option) -> Encoder { - if let Some(body) = body { - let can_chunked = head.version == Version::HTTP_11 - && (head.subject.0 != Method::HEAD) - && (head.subject.0 != Method::GET) - && (head.subject.0 != Method::CONNECT); - set_length(&mut head.headers, body, can_chunked) + let body = if let Some(body) = body { + body } else { head.headers.remove(header::TRANSFER_ENCODING); - Encoder::length(0) - } - } -} + return Encoder::length(0) + }; + + // HTTP/1.0 doesn't know about chunked + let can_chunked = head.version == Version::HTTP_11; + let headers = &mut head.headers; + + // If the user already set specific headers, we should respect them, regardless + // of what the Payload knows about itself. They set them for a reason. -fn set_length(headers: &mut HeaderMap, body: BodyLength, can_chunked: bool) -> Encoder { - // If the user already set specific headers, we should respect them, regardless - // of what the Payload knows about itself. They set them for a reason. + // Because of the borrow checker, we can't check the for an existing + // Content-Length header while holding an `Entry` for the Transfer-Encoding + // header, so unfortunately, we must do the check here, first. - // Because of the borrow checker, we can't check the for an existing - // Content-Length header while holding an `Entry` for the Transfer-Encoding - // header, so unfortunately, we must do the check here, first. + let existing_con_len = headers::content_length_parse_all(headers); + let mut should_remove_con_len = false; - let existing_con_len = headers::content_length_parse_all(headers); - let mut should_remove_con_len = false; + if !can_chunked { + // Chunked isn't legal, so if it is set, we need to remove it. + if headers.remove(header::TRANSFER_ENCODING).is_some() { + trace!("removing illegal transfer-encoding header"); + } + + return if let Some(len) = existing_con_len { + Encoder::length(len) + } else if let BodyLength::Known(len) = body { + set_content_length(headers, len) + } else { + // HTTP/1.0 client requests without a content-length + // cannot have any body at all. + Encoder::length(0) + }; + } - if can_chunked { // If the user set a transfer-encoding, respect that. Let's just // make sure `chunked` is the final encoding. let encoder = match headers.entry(header::TRANSFER_ENCODING) @@ -841,9 +854,22 @@ fn set_length(headers: &mut HeaderMap, body: BodyLength, can_chunked: bool) -> E if let Some(len) = existing_con_len { Some(Encoder::length(len)) } else if let BodyLength::Unknown = body { - should_remove_con_len = true; - te.insert(HeaderValue::from_static("chunked")); - Some(Encoder::chunked()) + // GET, HEAD, and CONNECT almost never have bodies. + // + // So instead of sending a "chunked" body with a 0-chunk, + // assume no body here. If you *must* send a body, + // set the headers explicitly. + match head.subject.0 { + Method::GET | + Method::HEAD | + Method::CONNECT => { + Some(Encoder::length(0)) + }, + _ => { + te.insert(HeaderValue::from_static("chunked")); + Some(Encoder::chunked()) + }, + } } else { None } @@ -869,27 +895,6 @@ fn set_length(headers: &mut HeaderMap, body: BodyLength, can_chunked: bool) -> E }; set_content_length(headers, len) - } else { - // Chunked isn't legal, so if it is set, we need to remove it. - // Also, if it *is* set, then we shouldn't replace with a length, - // since the user tried to imply there isn't a length. - let encoder = if headers.remove(header::TRANSFER_ENCODING).is_some() { - trace!("removing illegal transfer-encoding header"); - should_remove_con_len = true; - Encoder::close_delimited() - } else if let Some(len) = existing_con_len { - Encoder::length(len) - } else if let BodyLength::Known(len) = body { - set_content_length(headers, len) - } else { - Encoder::close_delimited() - }; - - if should_remove_con_len && existing_con_len.is_some() { - headers.remove(header::CONTENT_LENGTH); - } - - encoder } } diff --git a/tests/client.rs b/tests/client.rs index 54d7e1d650..b2d5fa08a5 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -356,7 +356,7 @@ test! { } test! { - name: client_get_implicitly_empty, + name: client_get_req_body_implicitly_empty, server: expected: "GET / HTTP/1.1\r\nhost: {addr}\r\n\r\n", @@ -370,9 +370,153 @@ test! { }, response: status: OK, + headers: {}, + body: None, +} + +test! { + name: client_get_req_body_chunked, + + server: + expected: "\ + GET / HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\ + host: {addr}\r\n\ + \r\n\ + 5\r\n\ + hello\r\n\ + 0\r\n\r\n\ + ", + reply: REPLY_OK, + + client: + request: { + method: GET, + url: "http://{addr}/", headers: { - "Content-Length" => "0", + "transfer-encoding" => "chunked", + }, + body: "hello", // not Body::empty + }, + response: + status: OK, + headers: {}, + body: None, +} + +test! { + name: client_get_req_body_chunked_http10, + + server: + expected: "\ + GET / HTTP/1.0\r\n\ + host: {addr}\r\n\ + content-length: 5\r\n\ + \r\n\ + hello\ + ", + reply: "HTTP/1.0 200 OK\r\ncontent-length: 0\r\n\r\n", + + client: + request: { + method: GET, + url: "http://{addr}/", + headers: { + "transfer-encoding" => "chunked", + }, + version: HTTP_10, + body: "hello", + }, + response: + status: OK, + headers: {}, + body: None, +} + +test! { + name: client_get_req_body_sized, + + server: + expected: "\ + GET / HTTP/1.1\r\n\ + content-length: 5\r\n\ + host: {addr}\r\n\ + \r\n\ + hello\ + ", + reply: REPLY_OK, + + client: + request: { + method: GET, + url: "http://{addr}/", + headers: { + "Content-Length" => "5", + }, + body: (Body::wrap_stream(Body::from("hello"))), + }, + response: + status: OK, + headers: {}, + body: None, +} + +test! { + name: client_get_req_body_unknown, + + server: + expected: "\ + GET / HTTP/1.1\r\n\ + host: {addr}\r\n\ + \r\n\ + ", + reply: REPLY_OK, + + client: + request: { + method: GET, + url: "http://{addr}/", + // wrap_steam means we don't know the content-length, + // but we're wrapping a non-empty stream. + // + // But since the headers cannot tell us, and the method typically + // doesn't have a body, the body must be ignored. + body: (Body::wrap_stream(Body::from("hello"))), + }, + response: + status: OK, + headers: {}, + body: None, +} + +test! { + name: client_get_req_body_unknown_http10, + + server: + expected: "\ + GET / HTTP/1.0\r\n\ + host: {addr}\r\n\ + \r\n\ + ", + reply: "HTTP/1.0 200 OK\r\ncontent-length: 0\r\n\r\n", + + client: + request: { + method: GET, + url: "http://{addr}/", + headers: { + "transfer-encoding" => "chunked", }, + version: HTTP_10, + // wrap_steam means we don't know the content-length, + // but we're wrapping a non-empty stream. + // + // But since the headers cannot tell us, the body must be ignored. + body: (Body::wrap_stream(Body::from("hello"))), + }, + response: + status: OK, + headers: {}, body: None, } @@ -434,6 +578,33 @@ test! { body: None, } +test! { + name: client_post_unknown, + + server: + expected: "\ + POST /chunks HTTP/1.1\r\n\ + host: {addr}\r\n\ + transfer-encoding: chunked\r\n\ + \r\n\ + B\r\n\ + foo bar baz\r\n\ + 0\r\n\r\n\ + ", + reply: REPLY_OK, + + client: + request: { + method: POST, + url: "http://{addr}/chunks", + body: (Body::wrap_stream(Body::from("foo bar baz"))), + }, + response: + status: OK, + headers: {}, + body: None, +} + test! { name: client_post_empty, From c83b54dc8877798e173287e35a414d53424e4185 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Wed, 4 Sep 2019 14:45:15 -0700 Subject: [PATCH 05/16] v0.12.34 --- CHANGELOG.md | 11 +++++++++++ Cargo.toml | 2 +- src/lib.rs | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4596c33bcd..07fcec6f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +### v0.12.34 (2019-09-04) + + +#### Bug Fixes + +* **client:** allow client GET requests with explicit body headers ([23fc8b08](https://github.com/hyperium/hyper/commit/23fc8b0806e7fde435ca00479cd5e3c8c5bdeee7), closes [#1925](https://github.com/hyperium/hyper/issues/1925)) + + +### v0.12.33 (2019-07-15) + + ### v0.12.32 (2019-07-08) diff --git a/Cargo.toml b/Cargo.toml index 45918ecfe8..5ba92212f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hyper" -version = "0.12.33" # don't forget to update html_root_url +version = "0.12.34" # don't forget to update html_root_url description = "A fast and correct HTTP library." readme = "README.md" homepage = "https://hyper.rs/" diff --git a/src/lib.rs b/src/lib.rs index aef1e45c09..0404a79829 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![doc(html_root_url = "https://docs.rs/hyper/0.12.33")] +#![doc(html_root_url = "https://docs.rs/hyper/0.12.34")] #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![cfg_attr(test, deny(warnings))] From 3286922460ab63d0a804d8170d862ff4ba5951dd Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Thu, 12 Sep 2019 17:46:00 -0700 Subject: [PATCH 06/16] feat(body): identify aborted body write errors (cherry picked from commit bb365f689defcf529bf546eb684177dbe801b0ba) --- src/body/body.rs | 2 +- src/error.rs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/body/body.rs b/src/body/body.rs index 00da58c5c6..9a1373cf91 100644 --- a/src/body/body.rs +++ b/src/body/body.rs @@ -247,7 +247,7 @@ impl Body { ref mut abort_rx, } => { if let Ok(Async::Ready(())) = abort_rx.poll() { - return Err(::Error::new_body_write("body write aborted")); + return Err(::Error::new_body_write_aborted()); } match rx.poll().expect("mpsc cannot error") { diff --git a/src/error.rs b/src/error.rs index 251d867152..c9d56db363 100644 --- a/src/error.rs +++ b/src/error.rs @@ -47,6 +47,8 @@ pub(crate) enum Kind { Body, /// Error while writing a body to connection. BodyWrite, + /// The body write was aborted. + BodyWriteAborted, /// Error calling AsyncWrite::shutdown() Shutdown, @@ -133,6 +135,11 @@ impl Error { self.inner.kind == Kind::IncompleteMessage } + /// Returns true if the body write was aborted. + pub fn is_body_write_aborted(&self) -> bool { + self.inner.kind == Kind::BodyWriteAborted + } + #[doc(hidden)] #[cfg_attr(error_source, deprecated(note = "use Error::source instead"))] pub fn cause2(&self) -> Option<&(dyn StdError + 'static + Sync + Send)> { @@ -250,6 +257,10 @@ impl Error { Error::new(Kind::BodyWrite).with(cause) } + pub(crate) fn new_body_write_aborted() -> Error { + Error::new(Kind::BodyWriteAborted) + } + fn new_user(user: User) -> Error { Error::new(Kind::User(user)) } @@ -352,6 +363,7 @@ impl StdError for Error { Kind::Accept => "error accepting connection", Kind::Body => "error reading a body from connection", Kind::BodyWrite => "error writing a body to connection", + Kind::BodyWriteAborted => "body write aborted", Kind::Shutdown => "error shutting down connection", Kind::Http2 => "http2 error", Kind::Io => "connection error", From c81f7eb9c2823b45531ac8aa318669818f197b92 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Fri, 13 Sep 2019 11:13:15 -0700 Subject: [PATCH 07/16] v0.12.35 --- CHANGELOG.md | 8 ++++++++ Cargo.toml | 2 +- src/lib.rs | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07fcec6f52..b64eb5ebc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### v0.12.35 (2019-09-13) + + +#### Features + +* **body:** identify aborted body write errors ([32869224](https://github.com/hyperium/hyper/commit/3286922460ab63d0a804d8170d862ff4ba5951dd)) + + ### v0.12.34 (2019-09-04) diff --git a/Cargo.toml b/Cargo.toml index 5ba92212f1..25df4bf68c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hyper" -version = "0.12.34" # don't forget to update html_root_url +version = "0.12.35" # don't forget to update html_root_url description = "A fast and correct HTTP library." readme = "README.md" homepage = "https://hyper.rs/" diff --git a/src/lib.rs b/src/lib.rs index 0404a79829..d5230b78ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![doc(html_root_url = "https://docs.rs/hyper/0.12.34")] +#![doc(html_root_url = "https://docs.rs/hyper/0.12.35")] #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![cfg_attr(test, deny(warnings))] From 4d188d17b36d840c624781d276c959a3ef4a1a63 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Thu, 3 Oct 2019 12:45:24 -0700 Subject: [PATCH 08/16] chore(ci): update MSRV to 1.31 (parking_lot uses 2018 edition) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6baedc9d3a..b35cbd3b2d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ matrix: - rust: stable env: FEATURES="--no-default-features" # Minimum Supported Rust Version - - rust: 1.27.0 + - rust: 1.31.0 env: FEATURES="--no-default-features --features runtime" BUILD_ONLY="1" before_script: From 90c1e8f44cb40d0eb1a2b36b2893e8bb5f970a96 Mon Sep 17 00:00:00 2001 From: Ben Boeckel Date: Mon, 14 Oct 2019 14:27:58 -0400 Subject: [PATCH 09/16] fix(dependencies): use correct minimum versions (#1974) `bytes` is bumped due to the addition of `Bytes::advance` in 0.4.6. `h2` 0.1.13 introduced the `.is_io()` and `into_io()` methods. Bumping `tokio-threadpool` to 0.1.16 removes a dependency on `rand` from the crate which reduces the size and avoids minimum version dependency issues in that dependency tree. --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 25df4bf68c..d990659355 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,13 +19,13 @@ include = [ ] [dependencies] -bytes = "0.4.4" +bytes = "0.4.6" futures = "0.1.21" futures-cpupool = { version = "0.1.6", optional = true } http = "0.1.15" http-body = "0.1" httparse = "1.0" -h2 = "0.1.10" +h2 = "0.1.13" iovec = "0.1" itoa = "0.4.1" log = "0.4" @@ -37,7 +37,7 @@ tokio-executor = { version = "0.1.0", optional = true } tokio-io = "0.1" tokio-reactor = { version = "0.1", optional = true } tokio-tcp = { version = "0.1", optional = true } -tokio-threadpool = { version = "0.1.3", optional = true } +tokio-threadpool = { version = "0.1.16", optional = true } tokio-timer = { version = "0.2", optional = true } want = "0.2" From 3d676fb775a8291cab1491a2c7a9a7f247749e63 Mon Sep 17 00:00:00 2001 From: James Le Cuirot Date: Tue, 22 Oct 2019 18:33:37 +0100 Subject: [PATCH 10/16] feat(client): Add connect timeout to HttpConnector This takes the same strategy as golang, where the timeout value is divided equally between the candidate socket addresses. If happy eyeballs is enabled, the division takes place "below" the IPv4/IPv6 partitioning. Backported to 0.12 from master. --- src/client/connect/dns.rs | 4 +++ src/client/connect/http.rs | 73 ++++++++++++++++++++++++++++++-------- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/client/connect/dns.rs b/src/client/connect/dns.rs index 28a7f2aa3b..efd798b523 100644 --- a/src/client/connect/dns.rs +++ b/src/client/connect/dns.rs @@ -261,6 +261,10 @@ impl IpAddrs { pub(super) fn is_empty(&self) -> bool { self.iter.as_slice().is_empty() } + + pub(super) fn len(&self) -> usize { + self.iter.as_slice().len() + } } impl Iterator for IpAddrs { diff --git a/src/client/connect/http.rs b/src/client/connect/http.rs index 16b23ff10e..a91026d72f 100644 --- a/src/client/connect/http.rs +++ b/src/client/connect/http.rs @@ -12,7 +12,7 @@ use http::uri::Scheme; use net2::TcpBuilder; use tokio_reactor::Handle; use tokio_tcp::{TcpStream, ConnectFuture}; -use tokio_timer::Delay; +use tokio_timer::{Delay, Timeout}; use super::{Connect, Connected, Destination}; use super::dns::{self, GaiResolver, Resolve, TokioThreadpoolGaiResolver}; @@ -29,6 +29,7 @@ use super::dns::{self, GaiResolver, Resolve, TokioThreadpoolGaiResolver}; pub struct HttpConnector { enforce_http: bool, handle: Option, + connect_timeout: Option, happy_eyeballs_timeout: Option, keep_alive_timeout: Option, local_address: Option, @@ -120,6 +121,7 @@ impl HttpConnector { HttpConnector { enforce_http: true, handle: None, + connect_timeout: None, happy_eyeballs_timeout: Some(Duration::from_millis(300)), keep_alive_timeout: None, local_address: None, @@ -187,6 +189,17 @@ impl HttpConnector { self.local_address = addr; } + /// Set the connect timeout. + /// + /// If a domain resolves to multiple IP addresses, the timeout will be + /// evenly divided across them. + /// + /// Default is `None`. + #[inline] + pub fn set_connect_timeout(&mut self, dur: Option) { + self.connect_timeout = dur; + } + /// Set timeout for [RFC 6555 (Happy Eyeballs)][RFC 6555] algorithm. /// /// If hostname resolves to both IPv4 and IPv6 addresses and connection @@ -259,6 +272,7 @@ where HttpConnecting { state: State::Lazy(self.resolver.clone(), host.into(), self.local_address), handle: self.handle.clone(), + connect_timeout: self.connect_timeout, happy_eyeballs_timeout: self.happy_eyeballs_timeout, keep_alive_timeout: self.keep_alive_timeout, nodelay: self.nodelay, @@ -285,6 +299,7 @@ fn invalid_url(err: InvalidUrl, handle: &Option) -> HttpConn keep_alive_timeout: None, nodelay: false, port: 0, + connect_timeout: None, happy_eyeballs_timeout: None, reuse_address: false, send_buffer_size: None, @@ -319,6 +334,7 @@ impl StdError for InvalidUrl { pub struct HttpConnecting { state: State, handle: Option, + connect_timeout: Option, happy_eyeballs_timeout: Option, keep_alive_timeout: Option, nodelay: bool, @@ -348,7 +364,7 @@ impl Future for HttpConnecting { // skip resolving the dns and start connecting right away. if let Some(addrs) = dns::IpAddrs::try_parse(host, self.port) { state = State::Connecting(ConnectingTcp::new( - local_addr, addrs, self.happy_eyeballs_timeout, self.reuse_address)); + local_addr, addrs, self.connect_timeout, self.happy_eyeballs_timeout, self.reuse_address)); } else { let name = dns::Name::new(mem::replace(host, String::new())); state = State::Resolving(resolver.resolve(name), local_addr); @@ -364,7 +380,7 @@ impl Future for HttpConnecting { .collect(); let addrs = dns::IpAddrs::new(addrs); state = State::Connecting(ConnectingTcp::new( - local_addr, addrs, self.happy_eyeballs_timeout, self.reuse_address)); + local_addr, addrs, self.connect_timeout, self.happy_eyeballs_timeout, self.reuse_address)); } }; }, @@ -417,6 +433,7 @@ impl ConnectingTcp { fn new( local_addr: Option, remote_addrs: dns::IpAddrs, + connect_timeout: Option, fallback_timeout: Option, reuse_address: bool, ) -> ConnectingTcp { @@ -425,7 +442,7 @@ impl ConnectingTcp { if fallback_addrs.is_empty() { return ConnectingTcp { local_addr, - preferred: ConnectingTcpRemote::new(preferred_addrs), + preferred: ConnectingTcpRemote::new(preferred_addrs, connect_timeout), fallback: None, reuse_address, }; @@ -433,17 +450,17 @@ impl ConnectingTcp { ConnectingTcp { local_addr, - preferred: ConnectingTcpRemote::new(preferred_addrs), + preferred: ConnectingTcpRemote::new(preferred_addrs, connect_timeout), fallback: Some(ConnectingTcpFallback { delay: Delay::new(Instant::now() + fallback_timeout), - remote: ConnectingTcpRemote::new(fallback_addrs), + remote: ConnectingTcpRemote::new(fallback_addrs, connect_timeout), }), reuse_address, } } else { ConnectingTcp { local_addr, - preferred: ConnectingTcpRemote::new(remote_addrs), + preferred: ConnectingTcpRemote::new(remote_addrs, connect_timeout), fallback: None, reuse_address, } @@ -458,13 +475,17 @@ struct ConnectingTcpFallback { struct ConnectingTcpRemote { addrs: dns::IpAddrs, - current: Option, + connect_timeout: Option, + current: Option, } impl ConnectingTcpRemote { - fn new(addrs: dns::IpAddrs) -> Self { + fn new(addrs: dns::IpAddrs, connect_timeout: Option) -> Self { + let connect_timeout = connect_timeout.map(|t| t / (addrs.len() as u32)); + Self { addrs, + connect_timeout, current: None, } } @@ -481,7 +502,18 @@ impl ConnectingTcpRemote { let mut err = None; loop { if let Some(ref mut current) = self.current { - match current.poll() { + let poll: Poll = match current { + MaybeTimedConnectFuture::Timed(future) => match future.poll() { + Ok(tcp) => Ok(tcp), + Err(err) => if err.is_inner() { + Err(err.into_inner().unwrap()) + } else { + Err(io::Error::new(io::ErrorKind::TimedOut, err.description())) + } + }, + MaybeTimedConnectFuture::Untimed(future) => future.poll(), + }; + match poll { Ok(Async::Ready(tcp)) => { debug!("connected to {:?}", tcp.peer_addr().ok()); return Ok(Async::Ready(tcp)); @@ -492,14 +524,14 @@ impl ConnectingTcpRemote { err = Some(e); if let Some(addr) = self.addrs.next() { debug!("connecting to {}", addr); - *current = connect(&addr, local_addr, handle, reuse_address)?; + *current = connect(&addr, local_addr, handle, reuse_address, self.connect_timeout)?; continue; } } } } else if let Some(addr) = self.addrs.next() { debug!("connecting to {}", addr); - self.current = Some(connect(&addr, local_addr, handle, reuse_address)?); + self.current = Some(connect(&addr, local_addr, handle, reuse_address, self.connect_timeout)?); continue; } @@ -508,7 +540,12 @@ impl ConnectingTcpRemote { } } -fn connect(addr: &SocketAddr, local_addr: &Option, handle: &Option, reuse_address: bool) -> io::Result { +enum MaybeTimedConnectFuture { + Timed(Timeout), + Untimed(ConnectFuture), +} + +fn connect(addr: &SocketAddr, local_addr: &Option, handle: &Option, reuse_address: bool, connect_timeout: Option) -> io::Result { let builder = match addr { &SocketAddr::V4(_) => TcpBuilder::new_v4()?, &SocketAddr::V6(_) => TcpBuilder::new_v6()?, @@ -540,7 +577,13 @@ fn connect(addr: &SocketAddr, local_addr: &Option, handle: &Option Cow::Owned(Handle::default()), }; - Ok(TcpStream::connect_std(builder.to_tcp_stream()?, addr, &handle)) + let stream = TcpStream::connect_std(builder.to_tcp_stream()?, addr, &handle); + + if let Some(timeout) = connect_timeout { + Ok(MaybeTimedConnectFuture::Timed(Timeout::new(stream, timeout))) + } else { + Ok(MaybeTimedConnectFuture::Untimed(stream)) + } } impl ConnectingTcp { @@ -706,7 +749,7 @@ mod tests { } let addrs = hosts.iter().map(|host| (host.clone(), addr.port()).into()).collect(); - let connecting_tcp = ConnectingTcp::new(None, dns::IpAddrs::new(addrs), Some(fallback_timeout), false); + let connecting_tcp = ConnectingTcp::new(None, dns::IpAddrs::new(addrs), None, Some(fallback_timeout), false); let fut = ConnectingTcpFuture(connecting_tcp); let start = Instant::now(); From da16ed62a3f478800f7a44c49acca4b109e4448b Mon Sep 17 00:00:00 2001 From: Peter Wilkins Date: Tue, 12 Nov 2019 18:08:36 +0000 Subject: [PATCH 11/16] fix(server): allow `Server::local_addr` to be called with custom executor (#2009) add generic for the executor to server impl block resolves https://github.com/hyperium/hyper/issues/1988 --- examples/single_threaded.rs | 7 ++++--- src/server/mod.rs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/single_threaded.rs b/examples/single_threaded.rs index d39158bc45..c0b8131840 100644 --- a/examples/single_threaded.rs +++ b/examples/single_threaded.rs @@ -37,14 +37,15 @@ fn main() { let server = Server::bind(&addr) .executor(exec) - .serve(new_service) - .map_err(|e| eprintln!("server error: {}", e)); + .serve(new_service); println!("Listening on http://{}", addr); + assert_eq!(addr, server.local_addr()); + current_thread::Runtime::new() .expect("rt new") - .spawn(server) + .spawn(server.map_err(|e| eprintln!("server error: {}", e))) .run() .expect("rt run"); } diff --git a/src/server/mod.rs b/src/server/mod.rs index b55af69875..57a394f10a 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -133,7 +133,7 @@ impl Server { } #[cfg(feature = "runtime")] -impl Server { +impl Server { /// Returns the local address that this server is bound to. pub fn local_addr(&self) -> SocketAddr { self.spawn_all.local_addr() From d4ee6996bf99eda110a067a80b5b7967fec6af48 Mon Sep 17 00:00:00 2001 From: James Le Cuirot Date: Tue, 12 Nov 2019 18:09:24 +0000 Subject: [PATCH 12/16] feat(client): add resolve timeout to HttpConnector (#1994) The recently-added connect timeout does not cover resolving hostnames, which could also stall on an OS-level timeout if there are issues reaching the DNS server. --- src/client/connect/http.rs | 45 ++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/client/connect/http.rs b/src/client/connect/http.rs index a91026d72f..f431f09c7c 100644 --- a/src/client/connect/http.rs +++ b/src/client/connect/http.rs @@ -29,6 +29,7 @@ use super::dns::{self, GaiResolver, Resolve, TokioThreadpoolGaiResolver}; pub struct HttpConnector { enforce_http: bool, handle: Option, + resolve_timeout: Option, connect_timeout: Option, happy_eyeballs_timeout: Option, keep_alive_timeout: Option, @@ -121,6 +122,7 @@ impl HttpConnector { HttpConnector { enforce_http: true, handle: None, + resolve_timeout: None, connect_timeout: None, happy_eyeballs_timeout: Some(Duration::from_millis(300)), keep_alive_timeout: None, @@ -189,6 +191,17 @@ impl HttpConnector { self.local_address = addr; } + /// Set timeout for hostname resolution. + /// + /// If `None`, then no timeout is applied by the connector, making it + /// subject to the timeout imposed by the operating system. + /// + /// Default is `None`. + #[inline] + pub fn set_resolve_timeout(&mut self, dur: Option) { + self.resolve_timeout = dur; + } + /// Set the connect timeout. /// /// If a domain resolves to multiple IP addresses, the timeout will be @@ -272,6 +285,7 @@ where HttpConnecting { state: State::Lazy(self.resolver.clone(), host.into(), self.local_address), handle: self.handle.clone(), + resolve_timeout: self.resolve_timeout, connect_timeout: self.connect_timeout, happy_eyeballs_timeout: self.happy_eyeballs_timeout, keep_alive_timeout: self.keep_alive_timeout, @@ -299,6 +313,7 @@ fn invalid_url(err: InvalidUrl, handle: &Option) -> HttpConn keep_alive_timeout: None, nodelay: false, port: 0, + resolve_timeout: None, connect_timeout: None, happy_eyeballs_timeout: None, reuse_address: false, @@ -334,6 +349,7 @@ impl StdError for InvalidUrl { pub struct HttpConnecting { state: State, handle: Option, + resolve_timeout: Option, connect_timeout: Option, happy_eyeballs_timeout: Option, keep_alive_timeout: Option, @@ -346,11 +362,16 @@ pub struct HttpConnecting { enum State { Lazy(R, String, Option), - Resolving(R::Future, Option), + Resolving(ResolvingFuture, Option), Connecting(ConnectingTcp), Error(Option), } +enum ResolvingFuture { + Timed(Timeout), + Untimed(R::Future), +} + impl Future for HttpConnecting { type Item = (TcpStream, Connected); type Error = io::Error; @@ -367,11 +388,27 @@ impl Future for HttpConnecting { local_addr, addrs, self.connect_timeout, self.happy_eyeballs_timeout, self.reuse_address)); } else { let name = dns::Name::new(mem::replace(host, String::new())); - state = State::Resolving(resolver.resolve(name), local_addr); + let future = resolver.resolve(name); + state = if let Some(timeout) = self.resolve_timeout { + State::Resolving(ResolvingFuture::Timed(Timeout::new(future, timeout)), local_addr) + } else { + State::Resolving(ResolvingFuture::Untimed(future), local_addr) + } } }, - State::Resolving(ref mut future, local_addr) => { - match future.poll()? { + State::Resolving(ref mut rfuture, local_addr) => { + let res: Async = match rfuture { + ResolvingFuture::Timed(future) => match future.poll() { + Ok(res) => res, + Err(err) => if err.is_inner() { + return Err(err.into_inner().unwrap()) + } else { + return Err(io::Error::new(io::ErrorKind::TimedOut, err.description())) + }, + }, + ResolvingFuture::Untimed(future) => future.poll()?, + }; + match res { Async::NotReady => return Ok(Async::NotReady), Async::Ready(addrs) => { let port = self.port; From a115c30f1c869a0027dc73157f11adf66b113dbb Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 30 Dec 2019 10:09:41 -0800 Subject: [PATCH 13/16] chore(lib): allow deprecations in 0.12.x builds --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index d5230b78ed..26278707bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ #![deny(missing_debug_implementations)] #![cfg_attr(test, deny(warnings))] #![cfg_attr(all(test, feature = "nightly"), feature(test))] +#![allow(deprecated)] //! # hyper //! From f605125067562174920206e57cb589d31e2afb4b Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Tue, 16 Feb 2021 11:49:28 +0100 Subject: [PATCH 14/16] fix(http1): fix server misinterpretting multiple Transfer-Encoding headers When a request arrived with multiple `Transfer-Encoding` headers, hyper would check each if they ended with `chunked`. It should have only checked if the *last* header ended with `chunked`. See GHSA-6hfq-h8hq-87mf --- src/proto/h1/role.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/proto/h1/role.rs b/src/proto/h1/role.rs index 902f93beaf..7193e1c573 100644 --- a/src/proto/h1/role.rs +++ b/src/proto/h1/role.rs @@ -170,6 +170,8 @@ impl Http1Transaction for Server { if headers::is_chunked_(&value) { is_te_chunked = true; decoder = DecodedLength::CHUNKED; + } else { + is_te_chunked = false; } }, header::CONTENT_LENGTH => { @@ -1226,6 +1228,15 @@ mod tests { \r\n\ ", "transfer-encoding doesn't end in chunked"); + parse_err( + "\ + POST / HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\ + transfer-encoding: afterlol\r\n\ + \r\n\ + ", + "transfer-encoding multiple lines doesn't end in chunked", + ); // http/1.0 From 6d9003d85bc650d5d7f548f7896cefb4e4f5c20b Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Wed, 17 Feb 2021 18:53:05 -0800 Subject: [PATCH 15/16] chore(lib): fix new unused variable lint --- src/service/service.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/service/service.rs b/src/service/service.rs index 41d47bd059..3302ff58bf 100644 --- a/src/service/service.rs +++ b/src/service/service.rs @@ -173,7 +173,6 @@ impl fmt::Debug for ServiceFnOk { } } -//#[cfg(test)] fn _assert_fn_mut() { fn assert_service(_t: &T) {} @@ -181,14 +180,14 @@ fn _assert_fn_mut() { let svc = service_fn(move |_req: Request<::Body>| { val += 1; - future::ok::<_, Never>(Response::new(::Body::empty())) + future::ok::<_, Never>(Response::new(::Body::from(val.to_string()))) }); assert_service(&svc); let svc = service_fn_ok(move |_req: Request<::Body>| { val += 1; - Response::new(::Body::empty()) + Response::new(::Body::from(val.to_string())) }); assert_service(&svc); From 4c82565f5a233b4f72ab6c745e9892d41b1bc274 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Wed, 17 Feb 2021 19:01:27 -0800 Subject: [PATCH 16/16] v0.12.36 --- CHANGELOG.md | 18 ++++++++++++++++++ Cargo.toml | 2 +- src/lib.rs | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b64eb5ebc5..61cad79cd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +### v0.12.36 (2021-02-17) + + +#### Bug Fixes + +* **client:** allow client GET requests with explicit body headers ([23fc8b08](https://github.com/hyperium/hyper/commit/23fc8b0806e7fde435ca00479cd5e3c8c5bdeee7), closes [#1925](https://github.com/hyperium/hyper/issues/1925)) +* **dependencies:** use correct minimum versions (#1974) ([90c1e8f4](https://github.com/hyperium/hyper/commit/90c1e8f44cb40d0eb1a2b36b2893e8bb5f970a96)) +* **http1:** fix server misinterpretting multiple Transfer-Encoding headers ([f6051250](https://github.com/hyperium/hyper/commit/f605125067562174920206e57cb589d31e2afb4b)) +* **server:** allow `Server::local_addr` to be called with custom executor (#2009) ([da16ed62](https://github.com/hyperium/hyper/commit/da16ed62a3f478800f7a44c49acca4b109e4448b)) + + +#### Features + +* **client:** + * add resolve timeout to HttpConnector (#1994) ([d4ee6996](https://github.com/hyperium/hyper/commit/d4ee6996bf99eda110a067a80b5b7967fec6af48)) + * Add connect timeout to HttpConnector ([3d676fb7](https://github.com/hyperium/hyper/commit/3d676fb775a8291cab1491a2c7a9a7f247749e63)) + + ### v0.12.35 (2019-09-13) diff --git a/Cargo.toml b/Cargo.toml index d990659355..bc8ebae409 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hyper" -version = "0.12.35" # don't forget to update html_root_url +version = "0.12.36" # don't forget to update html_root_url description = "A fast and correct HTTP library." readme = "README.md" homepage = "https://hyper.rs/" diff --git a/src/lib.rs b/src/lib.rs index 26278707bd..7f919ef344 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![doc(html_root_url = "https://docs.rs/hyper/0.12.35")] +#![doc(html_root_url = "https://docs.rs/hyper/0.12.36")] #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![cfg_attr(test, deny(warnings))]