1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-16 14:45:47 +02:00

Compare commits

...

17 Commits

Author SHA1 Message Date
Nikolay Kim
db0091ba6f disable server test for windows 2018-05-21 21:01:52 -07:00
Nikolay Kim
2159158c30 Fix streaming response with body compression 2018-05-21 20:50:10 -07:00
Nikolay Kim
76d790425f bump version 2018-05-21 19:07:56 -07:00
Nikolay Kim
90968d4333 Drop connection if request's payload is not fulle consumed #236 2018-05-21 18:54:17 -07:00
Nikolay Kim
577a509875 increase delay 2018-05-21 16:12:33 -07:00
Nikolay Kim
a9728abfc8 run coverage report on 1.24 2018-05-20 21:10:50 -07:00
Nikolay Kim
14d1b8e2b6 prepare release 2018-05-20 21:09:54 -07:00
Nikolay Kim
285c73e95e Re-use tcp listener on pause/resume 2018-05-20 20:47:20 -07:00
Nikolay Kim
483db7028c expose low level data 2018-05-20 20:37:19 -07:00
Nikolay Kim
082ff46041 Fix scope resource path extractor #234 2018-05-20 17:04:23 -07:00
Nikolay Kim
f32e8f22c8 Merge pull request #231 from qrvaelet/ranges
NamedFile: range upgrade
2018-05-20 09:18:46 -07:00
Nikolay Kim
766dde7c42 Merge branch 'master' into ranges 2018-05-20 08:51:07 -07:00
qrvaelet
b68687044e range header syntax fix, change range to content-range in responses, enabled accept ranges, tests for content-range, content-length, and range status code 2018-05-20 17:40:36 +02:00
Nikolay Kim
c9e84e9dd3 Merge pull request #233 from sindreij/patch-1
Fix some typos in server/srv.rs
2018-05-20 06:19:53 -07:00
Sindre Johansen
0126ac46fc Fix some typos in server/srv.rs
Hello! This looks like a great library, thanks for creating it! While reading through the documentation I found a few typos.
2018-05-20 14:43:26 +02:00
Nikolay Kim
9b7ea836d0 bump version 2018-05-17 18:34:09 -07:00
Nikolay Kim
537b420d35 Fix compilation with --no-default-features 2018-05-17 18:33:48 -07:00
16 changed files with 337 additions and 81 deletions

View File

@@ -31,12 +31,12 @@ before_script:
script:
- |
if [[ "$TRAVIS_RUST_VERSION" != "beta" ]]; then
if [[ "$TRAVIS_RUST_VERSION" != "1.24.0" ]]; then
cargo clean
cargo test --features="alpn,tls" -- --nocapture
fi
- |
if [[ "$TRAVIS_RUST_VERSION" == "beta" ]]; then
if [[ "$TRAVIS_RUST_VERSION" == "1.24.0" ]]; then
bash <(curl https://raw.githubusercontent.com/xd009642/tarpaulin/master/travis-install.sh)
USE_SKEPTIC=1 cargo tarpaulin --out Xml --no-count
bash <(curl -s https://codecov.io/bash)

View File

@@ -1,5 +1,24 @@
# Changes
## 0.6.9 (2018-05-22)
* Drop connection if request's payload is not fully consumed #236
* Fix streaming response with body compression
## 0.6.8 (2018-05-20)
* Fix scope resource path extractor #234
* Re-use tcp listener on pause/resume
## 0.6.7 (2018-05-17)
* Fix compilation with --no-default-features
## 0.6.6 (2018-05-17)
* Panic during middleware execution #226

View File

@@ -1,6 +1,6 @@
[package]
name = "actix-web"
version = "0.6.6"
version = "0.6.9"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
readme = "README.md"

137
src/fs.rs
View File

@@ -20,7 +20,7 @@ use mime_guess::{get_mime_type, guess_mime_type};
use error::Error;
use handler::{AsyncResult, Handler, Responder, RouteHandler, WrapHandler};
use header;
use http::{HttpRange, Method, StatusCode};
use http::{HttpRange, Method, StatusCode, ContentEncoding};
use httpmessage::HttpMessage;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
@@ -289,30 +289,29 @@ impl Responder for NamedFile {
resp.set(header::ETag(etag));
});
// TODO: Debug, enabling "accept-ranges: bytes" causes problems with
// certain clients when not using the ranges header.
//resp.header(header::ACCEPT_RANGES, format!("bytes"));
resp.header(header::ACCEPT_RANGES, "bytes");
let mut length = self.md.len();
let mut offset = 0;
// check for ranges header
// check for range header
if let Some(ranges) = req.headers().get(header::RANGE) {
if let Ok(rangesheader) = ranges.to_str() {
if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) {
length = rangesvec[0].length - 1;
length = rangesvec[0].length;
offset = rangesvec[0].start;
resp.content_encoding(ContentEncoding::Identity);
resp.header(
header::RANGE,
header::CONTENT_RANGE,
format!(
"bytes={}-{}/{}",
offset,
offset + length,
"bytes {}-{}/{}",
offset,
offset + length - 1,
self.md.len()
),
)
);
} else {
resp.header(header::RANGE, format!("*/{}", length));
resp.header(header::CONTENT_RANGE, format!("bytes */{}", length));
return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish());
};
} else {
@@ -778,6 +777,7 @@ mod tests {
App::new().handler("test", StaticFiles::new(".").index_file("Cargo.toml"))
});
// Valid range header
let request = srv
.get()
.uri(srv.url("/t%65st/Cargo.toml"))
@@ -787,10 +787,21 @@ mod tests {
let response = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
// Invalid range header
let request = srv
.get()
.uri(srv.url("/t%65st/Cargo.toml"))
.header(header::RANGE, "bytes=1-0")
.finish()
.unwrap();
let response = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
}
#[test]
fn test_named_file_ranges_headers() {
fn test_named_file_content_range_headers() {
let mut srv = test::TestServer::with_factory(|| {
App::new().handler(
"test",
@@ -798,13 +809,64 @@ mod tests {
)
});
// Valid range header
let request = srv
.get()
.uri(srv.url("/t%65st/tests/test.binary"))
.header(header::RANGE, "bytes=10-20")
.finish()
.unwrap();
let response = srv.execute(request.send()).unwrap();
let contentrange = response
.headers()
.get(header::CONTENT_RANGE)
.unwrap()
.to_str()
.unwrap();
assert_eq!(contentrange, "bytes 10-20/100");
// Invalid range header
let request = srv
.get()
.uri(srv.url("/t%65st/tests/test.binary"))
.header(header::RANGE, "bytes=10-5")
.finish()
.unwrap();
let response = srv.execute(request.send()).unwrap();
let contentrange = response
.headers()
.get(header::CONTENT_RANGE)
.unwrap()
.to_str()
.unwrap();
assert_eq!(contentrange, "bytes */100");
}
#[test]
fn test_named_file_content_length_headers() {
let mut srv = test::TestServer::with_factory(|| {
App::new().handler(
"test",
StaticFiles::new(".").index_file("tests/test.binary"),
)
});
// Valid range header
let request = srv
.get()
.uri(srv.url("/t%65st/tests/test.binary"))
.header(header::RANGE, "bytes=10-20")
.finish()
.unwrap();
let response = srv.execute(request.send()).unwrap();
let contentlength = response
.headers()
.get(header::CONTENT_LENGTH)
@@ -812,23 +874,62 @@ mod tests {
.to_str()
.unwrap();
assert_eq!(contentlength, "10");
assert_eq!(contentlength, "11");
// Invalid range header
let request = srv
.get()
.uri(srv.url("/t%65st/tests/test.binary"))
.header(header::RANGE, "bytes=10-20")
.header(header::RANGE, "bytes=10-8")
.finish()
.unwrap();
let response = srv.execute(request.send()).unwrap();
let range = response
let contentlength = response
.headers()
.get(header::RANGE)
.get(header::CONTENT_LENGTH)
.unwrap()
.to_str()
.unwrap();
assert_eq!(range, "bytes=10-20/100");
assert_eq!(contentlength, "0");
// Without range header
let request = srv
.get()
.uri(srv.url("/t%65st/tests/test.binary"))
.no_default_headers()
.finish()
.unwrap();
let response = srv.execute(request.send()).unwrap();
let contentlength = response
.headers()
.get(header::CONTENT_LENGTH)
.unwrap()
.to_str()
.unwrap();
assert_eq!(contentlength, "100");
// chunked
let request = srv
.get()
.uri(srv.url("/t%65st/tests/test.binary"))
.finish()
.unwrap();
let response = srv.execute(request.send()).unwrap();
let te = response
.headers()
.get(header::TRANSFER_ENCODING)
.unwrap()
.to_str()
.unwrap();
assert_eq!(te, "chunked");
}
#[test]

View File

@@ -58,6 +58,16 @@ impl<'a> Params<'a> {
self.0.push((name, value));
}
pub(crate) fn remove(&mut self, name: &str)
{
for idx in (0..self.0.len()).rev() {
if self.0[idx].0 == name {
self.0.remove(idx);
return
}
}
}
/// Check if there are any matched patterns
pub fn is_empty(&self) -> bool {
self.0.is_empty()

View File

@@ -336,6 +336,7 @@ impl<S: 'static> RouteHandler<S> for Scope<S> {
if pattern.match_with_params(path, req.match_info_mut()) {
let default = unsafe { &mut *self.default.as_ref().get() };
req.match_info_mut().remove("tail");
if self.middlewares.is_empty() {
let resource = unsafe { &mut *resource.get() };
return resource.handle(req, Some(default));

View File

@@ -33,6 +33,7 @@ pub(crate) enum PayloadType {
}
impl PayloadType {
#[cfg(any(feature = "brotli", feature = "flate2"))]
pub fn new(headers: &HeaderMap, sender: PayloadSender) -> PayloadType {
// check content-encoding
let enc = if let Some(enc) = headers.get(CONTENT_ENCODING) {
@@ -52,6 +53,11 @@ impl PayloadType {
_ => PayloadType::Encoding(Box::new(EncodedPayload::new(sender, enc))),
}
}
#[cfg(not(any(feature = "brotli", feature = "flate2")))]
pub fn new(headers: &HeaderMap, sender: PayloadSender) -> PayloadType {
PayloadType::Sender(sender)
}
}
impl PayloadWriter for PayloadType {
@@ -399,6 +405,7 @@ impl ContentEncoder {
// Enable content encoding only if response does not contain Content-Encoding
// header
#[cfg(any(feature = "brotli", feature = "flate2"))]
let mut encoding = if has_body {
let encoding = match response_encoding {
ContentEncoding::Auto => {
@@ -425,6 +432,8 @@ impl ContentEncoder {
} else {
ContentEncoding::Identity
};
#[cfg(not(any(feature = "brotli", feature = "flate2")))]
let mut encoding = ContentEncoding::Identity;
#[cfg_attr(feature = "cargo-clippy", allow(match_ref_pats))]
let mut transfer = match resp.body() {
@@ -435,40 +444,42 @@ impl ContentEncoder {
TransferEncoding::length(0, buf)
}
&Body::Binary(_) => {
if !(encoding == ContentEncoding::Identity
|| encoding == ContentEncoding::Auto)
#[cfg(any(feature = "brotli", feature = "flate2"))]
{
let tmp = SharedBytes::default();
let transfer = TransferEncoding::eof(tmp.clone());
let mut enc = match encoding {
#[cfg(feature = "flate2")]
ContentEncoding::Deflate => ContentEncoder::Deflate(
DeflateEncoder::new(transfer, Compression::fast()),
),
#[cfg(feature = "flate2")]
ContentEncoding::Gzip => ContentEncoder::Gzip(GzEncoder::new(
transfer,
Compression::fast(),
)),
#[cfg(feature = "brotli")]
ContentEncoding::Br => {
ContentEncoder::Br(BrotliEncoder::new(transfer, 3))
}
ContentEncoding::Identity | ContentEncoding::Auto => {
unreachable!()
}
};
if !(encoding == ContentEncoding::Identity
|| encoding == ContentEncoding::Auto)
{
let tmp = SharedBytes::default();
let transfer = TransferEncoding::eof(tmp.clone());
let mut enc = match encoding {
#[cfg(feature = "flate2")]
ContentEncoding::Deflate => ContentEncoder::Deflate(
DeflateEncoder::new(transfer, Compression::fast()),
),
#[cfg(feature = "flate2")]
ContentEncoding::Gzip => ContentEncoder::Gzip(
GzEncoder::new(transfer, Compression::fast()),
),
#[cfg(feature = "brotli")]
ContentEncoding::Br => {
ContentEncoder::Br(BrotliEncoder::new(transfer, 3))
}
ContentEncoding::Identity | ContentEncoding::Auto => {
unreachable!()
}
};
let bin = resp.replace_body(Body::Empty).binary();
let bin = resp.replace_body(Body::Empty).binary();
// TODO return error!
let _ = enc.write(bin);
let _ = enc.write_eof();
let body = tmp.take();
len = body.len();
// TODO return error!
let _ = enc.write(bin);
let _ = enc.write_eof();
let body = tmp.take();
len = body.len();
encoding = ContentEncoding::Identity;
resp.replace_body(Binary::from(body));
encoding = ContentEncoding::Identity;
resp.replace_body(Binary::from(body));
}
}
if is_head {
@@ -494,6 +505,11 @@ impl ContentEncoder {
}
TransferEncoding::eof(buf)
} else {
if !(encoding == ContentEncoding::Identity
|| encoding == ContentEncoding::Auto)
{
resp.headers_mut().remove(CONTENT_LENGTH);
}
ContentEncoder::streaming_encoding(buf, version, resp)
}
}

View File

@@ -270,7 +270,12 @@ where
debug!("Error sending data: {}", err);
return Err(());
}
_ => (),
Ok(Async::Ready(_)) => {
// non consumed payload in that case close connection
if self.payload.is_some() && self.tasks.is_empty() {
return Ok(Async::Ready(false))
}
}
}
}

View File

@@ -1,6 +1,6 @@
#![cfg_attr(feature = "cargo-clippy", allow(redundant_field_names))]
use bytes::BufMut;
use bytes::{BytesMut, BufMut};
use futures::{Async, Poll};
use std::io;
use std::rc::Rc;
@@ -45,7 +45,7 @@ impl<T: AsyncWrite, H: 'static> H1Writer<T, H> {
stream: T, buf: SharedBytes, settings: Rc<WorkerSettings<H>>,
) -> H1Writer<T, H> {
H1Writer {
flags: Flags::empty(),
flags: Flags::KEEPALIVE,
encoder: ContentEncoder::empty(buf.clone()),
written: 0,
headers_size: 0,
@@ -62,7 +62,7 @@ impl<T: AsyncWrite, H: 'static> H1Writer<T, H> {
pub fn reset(&mut self) {
self.written = 0;
self.flags = Flags::empty();
self.flags = Flags::KEEPALIVE;
}
pub fn disconnected(&mut self) {
@@ -100,6 +100,16 @@ impl<T: AsyncWrite, H: 'static> Writer for H1Writer<T, H> {
self.written
}
#[inline]
fn set_date(&self, dst: &mut BytesMut) {
self.settings.set_date(dst)
}
#[inline]
fn buffer(&self) -> &mut BytesMut {
self.buffer.get_mut()
}
fn start(
&mut self, req: &mut HttpInnerMessage, msg: &mut HttpResponse,
encoding: ContentEncoding,
@@ -108,9 +118,9 @@ impl<T: AsyncWrite, H: 'static> Writer for H1Writer<T, H> {
self.encoder =
ContentEncoder::for_server(self.buffer.clone(), req, msg, encoding);
if msg.keep_alive().unwrap_or_else(|| req.keep_alive()) {
self.flags.insert(Flags::STARTED | Flags::KEEPALIVE);
self.flags = Flags::STARTED | Flags::KEEPALIVE;
} else {
self.flags.insert(Flags::STARTED);
self.flags = Flags::STARTED;
}
// Connection upgrade

View File

@@ -71,6 +71,16 @@ impl<H: 'static> Writer for H2Writer<H> {
self.written
}
#[inline]
fn set_date(&self, dst: &mut BytesMut) {
self.settings.set_date(dst)
}
#[inline]
fn buffer(&self) -> &mut BytesMut {
self.buffer.get_mut()
}
fn start(
&mut self, req: &mut HttpInnerMessage, msg: &mut HttpResponse,
encoding: ContentEncoding,

View File

@@ -143,7 +143,7 @@ pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesM
}
/// NOTE: bytes object has to contain enough space
pub(crate) fn write_content_length(mut n: usize, bytes: &mut BytesMut) {
pub fn write_content_length(mut n: usize, bytes: &mut BytesMut) {
if n < 10 {
let mut buf: [u8; 21] = [
b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', b'e',

View File

@@ -3,7 +3,8 @@ use std::net::Shutdown;
use std::{io, time};
use actix;
use futures::Poll;
use bytes::BytesMut;
use futures::{Async, Poll};
use tokio_core::net::TcpStream;
use tokio_io::{AsyncRead, AsyncWrite};
@@ -24,6 +25,9 @@ mod worker;
pub use self::settings::ServerSettings;
pub use self::srv::HttpServer;
#[doc(hidden)]
pub use self::helpers::write_content_length;
use body::Binary;
use error::Error;
use header::ContentEncoding;
@@ -132,13 +136,15 @@ impl HttpHandler for Box<HttpHandler> {
#[doc(hidden)]
pub trait HttpHandlerTask {
/// Poll task, this method is used before or after *io* object is available
fn poll(&mut self) -> Poll<(), Error>;
fn poll(&mut self) -> Poll<(), Error>{
Ok(Async::Ready(()))
}
/// Poll task when *io* object is available
fn poll_io(&mut self, io: &mut Writer) -> Poll<bool, Error>;
/// Connection is disconnected
fn disconnected(&mut self);
fn disconnected(&mut self) {}
}
/// Conversion helper trait
@@ -168,8 +174,16 @@ pub enum WriterState {
#[doc(hidden)]
/// Stream writer
pub trait Writer {
/// number of bytes written to the stream
fn written(&self) -> u64;
#[doc(hidden)]
fn set_date(&self, st: &mut BytesMut);
#[doc(hidden)]
#[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref))]
fn buffer(&self) -> &mut BytesMut;
fn start(
&mut self, req: &mut HttpInnerMessage, resp: &mut HttpResponse,
encoding: ContentEncoding,

View File

@@ -309,7 +309,7 @@ where
/// The socket address to bind
///
/// To mind multiple addresses this method can be call multiple times.
/// To bind multiple addresses this method can be called multiple times.
pub fn bind<S: net::ToSocketAddrs>(mut self, addr: S) -> io::Result<Self> {
let sockets = self.bind2(addr)?;
self.sockets.extend(sockets);
@@ -319,7 +319,7 @@ where
#[cfg(feature = "tls")]
/// The ssl socket address to bind
///
/// To mind multiple addresses this method can be call multiple times.
/// To bind multiple addresses this method can be called multiple times.
pub fn bind_tls<S: net::ToSocketAddrs>(
mut self, addr: S, acceptor: TlsAcceptor,
) -> io::Result<Self> {
@@ -450,7 +450,6 @@ impl<H: IntoHttpHandler> HttpServer<H> {
self.accept.push(start_accept_thread(
token,
sock,
self.backlog,
tx.clone(),
socks.clone(),
workers.clone(),
@@ -782,7 +781,7 @@ enum Command {
}
fn start_accept_thread(
token: usize, sock: Socket, backlog: i32, srv: mpsc::UnboundedSender<ServerCommand>,
token: usize, sock: Socket, srv: mpsc::UnboundedSender<ServerCommand>,
socks: Slab<SocketInfo>,
mut workers: Vec<(usize, mpsc::UnboundedSender<Conn<net::TcpStream>>)>,
) -> (mio::SetReadiness, sync_mpsc::Sender<Command>) {
@@ -892,8 +891,8 @@ fn start_accept_thread(
},
CMD => match rx.try_recv() {
Ok(cmd) => match cmd {
Command::Pause => if let Some(server) = server.take() {
if let Err(err) = poll.deregister(&server) {
Command::Pause => if let Some(ref server) = server {
if let Err(err) = poll.deregister(server) {
error!(
"Can not deregister server socket {}",
err
@@ -906,15 +905,6 @@ fn start_accept_thread(
}
},
Command::Resume => {
let lst = create_tcp_listener(addr, backlog)
.expect("Can not create net::TcpListener");
server = Some(
mio::net::TcpListener::from_std(lst).expect(
"Can not create mio::net::TcpListener",
),
);
if let Some(ref server) = server {
if let Err(err) = poll.register(
server,

View File

@@ -239,9 +239,9 @@ impl StreamHandlerType {
match *self {
StreamHandlerType::Normal => "http",
#[cfg(feature = "tls")]
StreamHandlerType::Tls(ref acceptor) => "https",
StreamHandlerType::Tls(_) => "https",
#[cfg(feature = "alpn")]
StreamHandlerType::Alpn(ref acceptor) => "https",
StreamHandlerType::Alpn(_) => "https",
}
}
}

View File

@@ -368,6 +368,80 @@ fn test_path_and_query_extractor2_async4() {
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_scope_and_path_extractor() {
let mut srv = test::TestServer::with_factory(move || {
App::new().scope("/sc", |scope| {
scope.resource("/{num}/index.html", |r| {
r.route()
.with(|p: Path<(usize,)>| {
format!("Welcome {}!", p.0)
})
})
})
});
// client request
let request = srv
.get()
.uri(srv.url("/sc/10/index.html"))
.finish()
.unwrap();
let response = srv.execute(request.send()).unwrap();
assert!(response.status().is_success());
// read response
let bytes = srv.execute(response.body()).unwrap();
assert_eq!(bytes, Bytes::from_static(b"Welcome 10!"));
// client request
let request = srv
.get()
.uri(srv.url("/sc/test1/index.html"))
.finish()
.unwrap();
let response = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[test]
fn test_nested_scope_and_path_extractor() {
let mut srv = test::TestServer::with_factory(move || {
App::new().scope("/sc", |scope| {
scope.nested("/{num}", |scope| {
scope.resource("/{num}/index.html", |r| {
r.route()
.with(|p: Path<(usize, usize)>| {
format!("Welcome {} {}!", p.0, p.1)
})
})
})
})
});
// client request
let request = srv
.get()
.uri(srv.url("/sc/10/12/index.html"))
.finish()
.unwrap();
let response = srv.execute(request.send()).unwrap();
assert!(response.status().is_success());
// read response
let bytes = srv.execute(response.body()).unwrap();
assert_eq!(bytes, Bytes::from_static(b"Welcome 10 12!"));
// client request
let request = srv
.get()
.uri(srv.url("/sc/10/test1/index.html"))
.finish()
.unwrap();
let response = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[cfg(actix_impl_trait)]
fn test_impl_trait(
data: (Json<Value>, Path<PParam>, Query<PParam>),

View File

@@ -54,6 +54,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World";
#[test]
#[cfg(unix)]
fn test_start() {
let _ = test::TestServer::unused_addr();
let (tx, rx) = mpsc::channel();
@@ -86,12 +87,17 @@ fn test_start() {
// pause
let _ = srv_addr.send(server::PauseServer).wait();
thread::sleep(time::Duration::from_millis(200));
assert!(net::TcpStream::connect(addr).is_err());
{
let req = client::ClientRequest::get(format!("http://{}/", addr).as_str())
.timeout(time::Duration::from_millis(200))
.finish()
.unwrap();
assert!(sys.run_until_complete(req.send()).is_err());
}
// resume
let _ = srv_addr.send(server::ResumeServer).wait();
thread::sleep(time::Duration::from_millis(200));
thread::sleep(time::Duration::from_millis(400));
{
let req = client::ClientRequest::get(format!("http://{}/", addr).as_str())
.finish()