feat(lib): add support to disable tokio-proto internals
For now, this adds `client::Config::no_proto`, `server::Http::no_proto`, and `server::Server::no_proto` to skip tokio-proto implementations, and use an internal dispatch system instead. `Http::no_proto` is similar to `Http::bind_connection`, but returns a `Connection` that is a `Future` to drive HTTP with the provided service. Any errors prior to parsing a request, and after delivering a response (but before flush the response body) will be returned from this future. See #1342 for more.
This commit is contained in:
334
tests/client.rs
334
tests/client.rs
@@ -2,6 +2,7 @@
|
||||
extern crate hyper;
|
||||
extern crate futures;
|
||||
extern crate tokio_core;
|
||||
extern crate tokio_io;
|
||||
extern crate pretty_env_logger;
|
||||
|
||||
use std::io::{self, Read, Write};
|
||||
@@ -18,13 +19,24 @@ use futures::sync::oneshot;
|
||||
use tokio_core::reactor::{Core, Handle};
|
||||
|
||||
fn client(handle: &Handle) -> Client<HttpConnector> {
|
||||
Client::new(handle)
|
||||
let mut config = Client::configure();
|
||||
if env("HYPER_NO_PROTO", "1") {
|
||||
config = config.no_proto();
|
||||
}
|
||||
config.build(handle)
|
||||
}
|
||||
|
||||
fn s(buf: &[u8]) -> &str {
|
||||
::std::str::from_utf8(buf).unwrap()
|
||||
}
|
||||
|
||||
fn env(name: &str, val: &str) -> bool {
|
||||
match ::std::env::var(name) {
|
||||
Ok(var) => var == val,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! test {
|
||||
(
|
||||
name: $name:ident,
|
||||
@@ -49,51 +61,24 @@ macro_rules! test {
|
||||
#![allow(unused)]
|
||||
use hyper::header::*;
|
||||
let _ = pretty_env_logger::init();
|
||||
let server = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = server.local_addr().unwrap();
|
||||
let mut core = Core::new().unwrap();
|
||||
let client = client(&core.handle());
|
||||
let mut req = Request::new(Method::$client_method, format!($client_url, addr=addr).parse().unwrap());
|
||||
$(
|
||||
req.headers_mut().set($request_headers);
|
||||
)*
|
||||
|
||||
if let Some(body) = $request_body {
|
||||
let body: &'static str = body;
|
||||
req.set_body(body);
|
||||
}
|
||||
req.set_proxy($request_proxy);
|
||||
let res = test! {
|
||||
INNER;
|
||||
core: &mut core,
|
||||
server:
|
||||
expected: $server_expected,
|
||||
reply: $server_reply,
|
||||
client:
|
||||
request:
|
||||
method: $client_method,
|
||||
url: $client_url,
|
||||
headers: [ $($request_headers,)* ],
|
||||
body: $request_body,
|
||||
proxy: $request_proxy,
|
||||
}.unwrap();
|
||||
|
||||
let res = client.request(req);
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
let thread = thread::Builder::new()
|
||||
.name(format!("tcp-server<{}>", stringify!($name)));
|
||||
thread.spawn(move || {
|
||||
let mut inc = server.accept().unwrap().0;
|
||||
inc.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
|
||||
inc.set_write_timeout(Some(Duration::from_secs(5))).unwrap();
|
||||
let expected = format!($server_expected, addr=addr);
|
||||
let mut buf = [0; 4096];
|
||||
let mut n = 0;
|
||||
while n < buf.len() && n < expected.len() {
|
||||
n += match inc.read(&mut buf[n..]) {
|
||||
Ok(n) => n,
|
||||
Err(e) => panic!("failed to read request, partially read = {:?}, error: {}", s(&buf[..n]), e),
|
||||
};
|
||||
}
|
||||
assert_eq!(s(&buf[..n]), expected);
|
||||
|
||||
inc.write_all($server_reply.as_ref()).unwrap();
|
||||
let _ = tx.send(());
|
||||
}).unwrap();
|
||||
|
||||
let rx = rx.map_err(|_| hyper::Error::Io(io::Error::new(io::ErrorKind::Other, "thread panicked")));
|
||||
|
||||
let work = res.join(rx).map(|r| r.0);
|
||||
|
||||
let res = core.run(work).unwrap();
|
||||
assert_eq!(res.status(), StatusCode::$client_status);
|
||||
$(
|
||||
assert_eq!(res.headers().get(), Some(&$response_headers));
|
||||
@@ -106,6 +91,108 @@ macro_rules! test {
|
||||
assert_eq!(body.as_ref(), expected_res_body);
|
||||
}
|
||||
);
|
||||
(
|
||||
name: $name:ident,
|
||||
server:
|
||||
expected: $server_expected:expr,
|
||||
reply: $server_reply:expr,
|
||||
client:
|
||||
request:
|
||||
method: $client_method:ident,
|
||||
url: $client_url:expr,
|
||||
headers: [ $($request_headers:expr,)* ],
|
||||
body: $request_body:expr,
|
||||
proxy: $request_proxy:expr,
|
||||
|
||||
error: $err:expr,
|
||||
) => (
|
||||
#[test]
|
||||
fn $name() {
|
||||
#![allow(unused)]
|
||||
use hyper::header::*;
|
||||
let _ = pretty_env_logger::init();
|
||||
let mut core = Core::new().unwrap();
|
||||
|
||||
let err = test! {
|
||||
INNER;
|
||||
core: &mut core,
|
||||
server:
|
||||
expected: $server_expected,
|
||||
reply: $server_reply,
|
||||
client:
|
||||
request:
|
||||
method: $client_method,
|
||||
url: $client_url,
|
||||
headers: [ $($request_headers,)* ],
|
||||
body: $request_body,
|
||||
proxy: $request_proxy,
|
||||
}.unwrap_err();
|
||||
if !$err(&err) {
|
||||
panic!("unexpected error: {:?}", err)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
(
|
||||
INNER;
|
||||
core: $core:expr,
|
||||
server:
|
||||
expected: $server_expected:expr,
|
||||
reply: $server_reply:expr,
|
||||
client:
|
||||
request:
|
||||
method: $client_method:ident,
|
||||
url: $client_url:expr,
|
||||
headers: [ $($request_headers:expr,)* ],
|
||||
body: $request_body:expr,
|
||||
proxy: $request_proxy:expr,
|
||||
) => ({
|
||||
let server = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = server.local_addr().unwrap();
|
||||
let mut core = $core;
|
||||
let client = client(&core.handle());
|
||||
let mut req = Request::new(Method::$client_method, format!($client_url, addr=addr).parse().unwrap());
|
||||
$(
|
||||
req.headers_mut().set($request_headers);
|
||||
)*
|
||||
|
||||
if let Some(body) = $request_body {
|
||||
let body: &'static str = body;
|
||||
req.set_body(body);
|
||||
}
|
||||
req.set_proxy($request_proxy);
|
||||
|
||||
let res = client.request(req);
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
let thread = thread::Builder::new()
|
||||
.name(format!("tcp-server<{}>", stringify!($name)));
|
||||
thread.spawn(move || {
|
||||
let mut inc = server.accept().unwrap().0;
|
||||
inc.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
|
||||
inc.set_write_timeout(Some(Duration::from_secs(5))).unwrap();
|
||||
let expected = format!($server_expected, addr=addr);
|
||||
let mut buf = [0; 4096];
|
||||
let mut n = 0;
|
||||
while n < buf.len() && n < expected.len() {
|
||||
n += match inc.read(&mut buf[n..]) {
|
||||
Ok(n) => n,
|
||||
Err(e) => panic!("failed to read request, partially read = {:?}, error: {}", s(&buf[..n]), e),
|
||||
};
|
||||
}
|
||||
assert_eq!(s(&buf[..n]), expected);
|
||||
|
||||
inc.write_all($server_reply.as_ref()).unwrap();
|
||||
let _ = tx.send(());
|
||||
}).unwrap();
|
||||
|
||||
let rx = rx.map_err(|_| hyper::Error::Io(io::Error::new(io::ErrorKind::Other, "thread panicked")));
|
||||
|
||||
let work = res.join(rx).map(|r| r.0);
|
||||
|
||||
core.run(work)
|
||||
});
|
||||
}
|
||||
|
||||
static REPLY_OK: &'static str = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
|
||||
@@ -266,6 +353,61 @@ test! {
|
||||
body: None,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: client_error_unexpected_eof,
|
||||
|
||||
server:
|
||||
expected: "\
|
||||
GET /err HTTP/1.1\r\n\
|
||||
Host: {addr}\r\n\
|
||||
\r\n\
|
||||
",
|
||||
reply: "\
|
||||
HTTP/1.1 200 OK\r\n\
|
||||
", // unexpected eof before double CRLF
|
||||
|
||||
client:
|
||||
request:
|
||||
method: Get,
|
||||
url: "http://{addr}/err",
|
||||
headers: [],
|
||||
body: None,
|
||||
proxy: false,
|
||||
error: |err| match err {
|
||||
&hyper::Error::Io(_) => true,
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
|
||||
test! {
|
||||
name: client_error_parse_version,
|
||||
|
||||
server:
|
||||
expected: "\
|
||||
GET /err HTTP/1.1\r\n\
|
||||
Host: {addr}\r\n\
|
||||
\r\n\
|
||||
",
|
||||
reply: "\
|
||||
HEAT/1.1 200 OK\r\n\
|
||||
\r\n\
|
||||
",
|
||||
|
||||
client:
|
||||
request:
|
||||
method: Get,
|
||||
url: "http://{addr}/err",
|
||||
headers: [],
|
||||
body: None,
|
||||
proxy: false,
|
||||
error: |err| match err {
|
||||
&hyper::Error::Version if env("HYPER_NO_PROTO", "1") => true,
|
||||
&hyper::Error::Io(_) if !env("HYPER_NO_PROTO", "1") => true,
|
||||
_ => false,
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_keep_alive() {
|
||||
let server = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
@@ -285,9 +427,10 @@ fn client_keep_alive() {
|
||||
sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n").expect("write 1");
|
||||
let _ = tx1.send(());
|
||||
|
||||
sock.read(&mut buf).expect("read 2");
|
||||
let second_get = b"GET /b HTTP/1.1\r\n";
|
||||
assert_eq!(&buf[..second_get.len()], second_get);
|
||||
let n2 = sock.read(&mut buf).expect("read 2");
|
||||
assert_ne!(n2, 0);
|
||||
let second_get = "GET /b HTTP/1.1\r\n";
|
||||
assert_eq!(s(&buf[..second_get.len()]), second_get);
|
||||
sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n").expect("write 2");
|
||||
let _ = tx2.send(());
|
||||
});
|
||||
@@ -367,3 +510,104 @@ fn client_pooled_socket_disconnected() {
|
||||
assert_ne!(addr1, addr2);
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn drop_body_before_eof_closes_connection() {
|
||||
// https://github.com/hyperium/hyper/issues/1353
|
||||
use std::io::{self, Read, Write};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio_core::reactor::{Timeout};
|
||||
use tokio_core::net::TcpStream;
|
||||
use tokio_io::{AsyncRead, AsyncWrite};
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::server::Service;
|
||||
use hyper::Uri;
|
||||
|
||||
let _ = pretty_env_logger::init();
|
||||
|
||||
let server = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = server.local_addr().unwrap();
|
||||
let mut core = Core::new().unwrap();
|
||||
let handle = core.handle();
|
||||
let closes = Arc::new(AtomicUsize::new(0));
|
||||
let client = Client::configure()
|
||||
.connector(DebugConnector(HttpConnector::new(1, &core.handle()), closes.clone()))
|
||||
.no_proto()
|
||||
.build(&handle);
|
||||
|
||||
let (tx1, rx1) = oneshot::channel();
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut sock = server.accept().unwrap().0;
|
||||
sock.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
|
||||
sock.set_write_timeout(Some(Duration::from_secs(5))).unwrap();
|
||||
let mut buf = [0; 4096];
|
||||
sock.read(&mut buf).expect("read 1");
|
||||
let body = vec![b'x'; 1024 * 128];
|
||||
write!(sock, "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", body.len()).expect("write head");
|
||||
let _ = sock.write_all(&body);
|
||||
let _ = tx1.send(());
|
||||
});
|
||||
|
||||
let uri = format!("http://{}/a", addr).parse().unwrap();
|
||||
|
||||
let res = client.get(uri).and_then(move |res| {
|
||||
assert_eq!(res.status(), hyper::StatusCode::Ok);
|
||||
Timeout::new(Duration::from_secs(1), &handle).unwrap()
|
||||
.from_err()
|
||||
});
|
||||
let rx = rx1.map_err(|_| hyper::Error::Io(io::Error::new(io::ErrorKind::Other, "thread panicked")));
|
||||
core.run(res.join(rx).map(|r| r.0)).unwrap();
|
||||
|
||||
assert_eq!(closes.load(Ordering::Relaxed), 1);
|
||||
|
||||
|
||||
|
||||
struct DebugConnector(HttpConnector, Arc<AtomicUsize>);
|
||||
|
||||
impl Service for DebugConnector {
|
||||
type Request = Uri;
|
||||
type Response = DebugStream;
|
||||
type Error = io::Error;
|
||||
type Future = Box<Future<Item = DebugStream, Error = io::Error>>;
|
||||
|
||||
fn call(&self, uri: Uri) -> Self::Future {
|
||||
let counter = self.1.clone();
|
||||
Box::new(self.0.call(uri).map(move |s| DebugStream(s, counter)))
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugStream(TcpStream, Arc<AtomicUsize>);
|
||||
|
||||
impl Drop for DebugStream {
|
||||
fn drop(&mut self) {
|
||||
self.1.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for DebugStream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.0.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.0.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for DebugStream {
|
||||
fn shutdown(&mut self) -> futures::Poll<(), io::Error> {
|
||||
AsyncWrite::shutdown(&mut self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for DebugStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.0.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for DebugStream {}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@ extern crate hyper;
|
||||
extern crate futures;
|
||||
extern crate spmc;
|
||||
extern crate pretty_env_logger;
|
||||
extern crate tokio_core;
|
||||
|
||||
use futures::{Future, Stream};
|
||||
use futures::future::{self, FutureResult};
|
||||
use futures::sync::oneshot;
|
||||
|
||||
use tokio_core::net::TcpListener;
|
||||
use tokio_core::reactor::Core;
|
||||
|
||||
use std::net::{TcpStream, SocketAddr};
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::mpsc;
|
||||
@@ -387,6 +392,7 @@ fn disable_keep_alive() {
|
||||
.header(hyper::header::ContentLength(quux.len() as u64))
|
||||
.body(quux);
|
||||
|
||||
// the write can possibly succeed, since it fills the kernel buffer on the first write
|
||||
let _ = req.write_all(b"\
|
||||
GET /quux HTTP/1.1\r\n\
|
||||
Host: example.domain\r\n\
|
||||
@@ -394,7 +400,6 @@ fn disable_keep_alive() {
|
||||
\r\n\
|
||||
");
|
||||
|
||||
// the write can possibly succeed, since it fills the kernel buffer on the first write
|
||||
let mut buf = [0; 1024 * 8];
|
||||
match req.read(&mut buf[..]) {
|
||||
// Ok(0) means EOF, so a proper shutdown
|
||||
@@ -504,6 +509,50 @@ fn pipeline_enabled() {
|
||||
assert_eq!(n, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_proto_empty_parse_eof_does_not_return_error() {
|
||||
let mut core = Core::new().unwrap();
|
||||
let listener = TcpListener::bind(&"127.0.0.1:0".parse().unwrap(), &core.handle()).unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
thread::spawn(move || {
|
||||
let _tcp = connect(&addr);
|
||||
});
|
||||
|
||||
let fut = listener.incoming()
|
||||
.into_future()
|
||||
.map_err(|_| unreachable!())
|
||||
.and_then(|(item, _incoming)| {
|
||||
let (socket, _) = item.unwrap();
|
||||
Http::new().no_proto(socket, HelloWorld)
|
||||
});
|
||||
|
||||
core.run(fut).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_proto_nonempty_parse_eof_returns_error() {
|
||||
let mut core = Core::new().unwrap();
|
||||
let listener = TcpListener::bind(&"127.0.0.1:0".parse().unwrap(), &core.handle()).unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut tcp = connect(&addr);
|
||||
tcp.write_all(b"GET / HTTP/1.1").unwrap();
|
||||
});
|
||||
|
||||
let fut = listener.incoming()
|
||||
.into_future()
|
||||
.map_err(|_| unreachable!())
|
||||
.and_then(|(item, _incoming)| {
|
||||
let (socket, _) = item.unwrap();
|
||||
Http::new().no_proto(socket, HelloWorld)
|
||||
.map(|_| ())
|
||||
});
|
||||
|
||||
core.run(fut).unwrap_err();
|
||||
}
|
||||
|
||||
// -------------------------------------------------
|
||||
// the Server that is used to run all the tests with
|
||||
// -------------------------------------------------
|
||||
@@ -628,6 +677,20 @@ impl Service for TestService {
|
||||
|
||||
}
|
||||
|
||||
struct HelloWorld;
|
||||
|
||||
impl Service for HelloWorld {
|
||||
type Request = Request;
|
||||
type Response = Response;
|
||||
type Error = hyper::Error;
|
||||
type Future = FutureResult<Self::Response, Self::Error>;
|
||||
|
||||
fn call(&self, _req: Request) -> Self::Future {
|
||||
future::ok(Response::new())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn connect(addr: &SocketAddr) -> TcpStream {
|
||||
let req = TcpStream::connect(addr).unwrap();
|
||||
req.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
|
||||
@@ -639,13 +702,31 @@ fn serve() -> Serve {
|
||||
serve_with_options(Default::default())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ServeOptions {
|
||||
keep_alive_disabled: bool,
|
||||
no_proto: bool,
|
||||
pipeline: bool,
|
||||
timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Default for ServeOptions {
|
||||
fn default() -> Self {
|
||||
ServeOptions {
|
||||
keep_alive_disabled: false,
|
||||
no_proto: env("HYPER_NO_PROTO", "1"),
|
||||
pipeline: false,
|
||||
timeout: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn env(name: &str, val: &str) -> bool {
|
||||
match ::std::env::var(name) {
|
||||
Ok(var) => var == val,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn serve_with_options(options: ServeOptions) -> Serve {
|
||||
let _ = pretty_env_logger::init();
|
||||
|
||||
@@ -657,12 +738,13 @@ fn serve_with_options(options: ServeOptions) -> Serve {
|
||||
let addr = "127.0.0.1:0".parse().unwrap();
|
||||
|
||||
let keep_alive = !options.keep_alive_disabled;
|
||||
let no_proto = !options.no_proto;
|
||||
let pipeline = options.pipeline;
|
||||
let dur = options.timeout;
|
||||
|
||||
let thread_name = format!("test-server-{:?}", dur);
|
||||
let thread = thread::Builder::new().name(thread_name).spawn(move || {
|
||||
let srv = Http::new()
|
||||
let mut srv = Http::new()
|
||||
.keep_alive(keep_alive)
|
||||
.pipeline(pipeline)
|
||||
.bind(&addr, TestService {
|
||||
@@ -670,6 +752,9 @@ fn serve_with_options(options: ServeOptions) -> Serve {
|
||||
_timeout: dur,
|
||||
reply: reply_rx,
|
||||
}).unwrap();
|
||||
if no_proto {
|
||||
srv.no_proto();
|
||||
}
|
||||
addr_tx.send(srv.local_addr().unwrap()).unwrap();
|
||||
srv.run_until(shutdown_rx.then(|_| Ok(()))).unwrap();
|
||||
}).unwrap();
|
||||
@@ -684,5 +769,3 @@ fn serve_with_options(options: ServeOptions) -> Serve {
|
||||
thread: Some(thread),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user