mod test_utils; use std::io::{Read, Write}; use std::net::{SocketAddr, TcpListener}; use std::pin::Pin; use std::sync::atomic::Ordering; use std::sync::Arc; use std::task::Poll; use std::thread; use std::time::Duration; use futures_channel::{mpsc, oneshot}; use futures_util::future::{self, FutureExt, TryFutureExt}; use futures_util::stream::StreamExt; use futures_util::{self, Stream}; use http_body_util::BodyExt; use http_body_util::{Empty, Full, StreamBody}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use hyper::body::Bytes; use hyper::body::Frame; use hyper::Request; use hyper_util::client::legacy::connect::{capture_connection, HttpConnector}; use hyper_util::client::legacy::Client; use hyper_util::rt::{TokioExecutor, TokioIo}; use test_utils::{DebugConnector, DebugStream}; pub fn runtime() -> tokio::runtime::Runtime { tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("new rt") } fn s(buf: &[u8]) -> &str { std::str::from_utf8(buf).expect("from_utf8") } #[cfg(not(miri))] #[test] fn drop_body_before_eof_closes_connection() { // https://github.com/hyperium/hyper/issues/1353 let _ = pretty_env_logger::try_init(); let server = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = server.local_addr().unwrap(); let rt = runtime(); let (closes_tx, closes) = mpsc::channel::<()>(10); let client = Client::builder(hyper_util::rt::TokioExecutor::new()).build( DebugConnector::with_http_and_closes(HttpConnector::new(), closes_tx), ); 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 req = Request::builder() .uri(&*format!("http://{addr}/a")) .body(Empty::::new()) .unwrap(); let res = client.request(req).map_ok(move |res| { assert_eq!(res.status(), hyper::StatusCode::OK); }); let rx = rx1; rt.block_on(async move { let (res, _) = future::join(res, rx).await; res.unwrap(); tokio::time::sleep(Duration::from_secs(1)).await; }); rt.block_on(closes.into_future()).0.expect("closes"); } #[cfg(not(miri))] #[tokio::test] async fn drop_client_closes_idle_connections() { let _ = pretty_env_logger::try_init(); let server = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = server.local_addr().unwrap(); let (closes_tx, mut closes) = mpsc::channel(10); let (tx1, rx1) = oneshot::channel(); let t1 = tokio::spawn(async move { let mut sock = server.accept().await.unwrap().0; let mut buf = [0; 4096]; sock.read(&mut buf).await.expect("read 1"); let body = [b'x'; 64]; let headers = format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", body.len()); sock.write_all(headers.as_bytes()) .await .expect("write head"); sock.write_all(&body).await.expect("write body"); let _ = tx1.send(()); // prevent this thread from closing until end of test, so the connection // stays open and idle until Client is dropped if let Ok(n) = sock.read(&mut buf).await { assert_eq!(n, 0); } }); let client = Client::builder(TokioExecutor::new()).build(DebugConnector::with_http_and_closes( HttpConnector::new(), closes_tx, )); let req = Request::builder() .uri(&*format!("http://{addr}/a")) .body(Empty::::new()) .unwrap(); let res = client.request(req).map_ok(move |res| { assert_eq!(res.status(), hyper::StatusCode::OK); }); let rx = rx1; let (res, _) = future::join(res, rx).await; res.unwrap(); // not closed yet, just idle std::future::poll_fn(|ctx| { assert!(Pin::new(&mut closes).poll_next(ctx).is_pending()); Poll::Ready(()) }) .await; // drop to start the connections closing drop(client); // and wait a few ticks for the connections to close let t = tokio::time::sleep(Duration::from_millis(100)).map(|_| panic!("time out")); futures_util::pin_mut!(t); let close = closes.into_future().map(|(opt, _)| opt.expect("closes")); future::select(t, close).await; t1.await.unwrap(); } #[cfg(not(miri))] #[tokio::test] async fn drop_response_future_closes_in_progress_connection() { let _ = pretty_env_logger::try_init(); let server = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = server.local_addr().unwrap(); let (closes_tx, closes) = mpsc::channel(10); let (tx1, rx1) = oneshot::channel(); let (_client_drop_tx, client_drop_rx) = std::sync::mpsc::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"); // we never write a response head // simulates a slow server operation let _ = tx1.send(()); // prevent this thread from closing until end of test, so the connection // stays open and idle until Client is dropped let _ = client_drop_rx.recv(); }); let res = { let client = Client::builder(TokioExecutor::new()).build( DebugConnector::with_http_and_closes(HttpConnector::new(), closes_tx), ); let req = Request::builder() .uri(&*format!("http://{addr}/a")) .body(Empty::::new()) .unwrap(); client.request(req).map(|_| unreachable!()) }; future::select(res, rx1).await; // res now dropped let t = tokio::time::sleep(Duration::from_millis(100)).map(|_| panic!("time out")); futures_util::pin_mut!(t); let close = closes.into_future().map(|(opt, _)| opt.expect("closes")); future::select(t, close).await; } #[cfg(not(miri))] #[tokio::test] async fn drop_response_body_closes_in_progress_connection() { let _ = pretty_env_logger::try_init(); let server = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = server.local_addr().unwrap(); let (closes_tx, closes) = mpsc::channel(10); let (tx1, rx1) = oneshot::channel(); let (_client_drop_tx, client_drop_rx) = std::sync::mpsc::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"); write!( sock, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" ) .expect("write head"); let _ = tx1.send(()); // prevent this thread from closing until end of test, so the connection // stays open and idle until Client is dropped let _ = client_drop_rx.recv(); }); let rx = rx1; let res = { let client = Client::builder(TokioExecutor::new()).build( DebugConnector::with_http_and_closes(HttpConnector::new(), closes_tx), ); let req = Request::builder() .uri(&*format!("http://{addr}/a")) .body(Empty::::new()) .unwrap(); // notably, haven't read body yet client.request(req) }; let (res, _) = future::join(res, rx).await; // drop the body res.unwrap(); // and wait a few ticks to see the connection drop let t = tokio::time::sleep(Duration::from_millis(100)).map(|_| panic!("time out")); futures_util::pin_mut!(t); let close = closes.into_future().map(|(opt, _)| opt.expect("closes")); future::select(t, close).await; } #[cfg(not(miri))] #[tokio::test] async fn no_keep_alive_closes_connection() { // https://github.com/hyperium/hyper/issues/1383 let _ = pretty_env_logger::try_init(); let server = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = server.local_addr().unwrap(); let (closes_tx, closes) = mpsc::channel(10); let (tx1, rx1) = oneshot::channel(); let (_tx2, rx2) = std::sync::mpsc::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"); sock.write(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") .unwrap(); let _ = tx1.send(()); // prevent this thread from closing until end of test, so the connection // stays open and idle until Client is dropped let _ = rx2.recv(); }); let client = Client::builder(TokioExecutor::new()) .pool_max_idle_per_host(0) .build(DebugConnector::with_http_and_closes( HttpConnector::new(), closes_tx, )); let req = Request::builder() .uri(&*format!("http://{addr}/a")) .body(Empty::::new()) .unwrap(); let res = client.request(req).map_ok(move |res| { assert_eq!(res.status(), hyper::StatusCode::OK); }); let rx = rx1; let (res, _) = future::join(res, rx).await; res.unwrap(); let t = tokio::time::sleep(Duration::from_millis(100)).map(|_| panic!("time out")); futures_util::pin_mut!(t); let close = closes.into_future().map(|(opt, _)| opt.expect("closes")); future::select(close, t).await; } #[cfg(not(miri))] #[tokio::test] async fn socket_disconnect_closes_idle_conn() { // notably when keep-alive is enabled let _ = pretty_env_logger::try_init(); let server = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = server.local_addr().unwrap(); let (closes_tx, closes) = mpsc::channel(10); 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"); sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") .unwrap(); let _ = tx1.send(()); }); let client = Client::builder(TokioExecutor::new()).build(DebugConnector::with_http_and_closes( HttpConnector::new(), closes_tx, )); let req = Request::builder() .uri(&*format!("http://{addr}/a")) .body(Empty::::new()) .unwrap(); let res = client.request(req).map_ok(move |res| { assert_eq!(res.status(), hyper::StatusCode::OK); }); let rx = rx1; let (res, _) = future::join(res, rx).await; res.unwrap(); let t = tokio::time::sleep(Duration::from_millis(100)).map(|_| panic!("time out")); futures_util::pin_mut!(t); let close = closes.into_future().map(|(opt, _)| opt.expect("closes")); future::select(t, close).await; } #[test] fn connect_call_is_lazy() { // We especially don't want connects() triggered if there's // idle connections that the Checkout would have found let _ = pretty_env_logger::try_init(); let _rt = runtime(); let connector = DebugConnector::new(); let connects = connector.connects.clone(); let client = Client::builder(TokioExecutor::new()).build(connector); assert_eq!(connects.load(Ordering::Relaxed), 0); let req = Request::builder() .uri("http://hyper.local/a") .body(Empty::::new()) .unwrap(); let _fut = client.request(req); // internal Connect::connect should have been lazy, and not // triggered an actual connect yet. assert_eq!(connects.load(Ordering::Relaxed), 0); } #[cfg(not(miri))] #[test] fn client_keep_alive_0() { let _ = pretty_env_logger::try_init(); let server = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = server.local_addr().unwrap(); let rt = runtime(); let connector = DebugConnector::new(); let connects = connector.connects.clone(); let client = Client::builder(TokioExecutor::new()).build(connector); let (tx1, rx1) = oneshot::channel(); let (tx2, rx2) = oneshot::channel(); thread::spawn(move || { let mut sock = server.accept().unwrap().0; //drop(server); 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"); sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") .expect("write 1"); let _ = tx1.send(()); 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(()); }); assert_eq!(connects.load(Ordering::SeqCst), 0); let rx = rx1; let req = Request::builder() .uri(&*format!("http://{addr}/a")) .body(Empty::::new()) .unwrap(); let res = client.request(req); rt.block_on(future::join(res, rx).map(|r| r.0)).unwrap(); assert_eq!(connects.load(Ordering::SeqCst), 1); // sleep real quick to let the threadpool put connection in ready // state and back into client pool thread::sleep(Duration::from_millis(50)); let rx = rx2; let req = Request::builder() .uri(&*format!("http://{addr}/b")) .body(Empty::::new()) .unwrap(); let res = client.request(req); rt.block_on(future::join(res, rx).map(|r| r.0)).unwrap(); assert_eq!( connects.load(Ordering::SeqCst), 1, "second request should still only have 1 connect" ); drop(client); } #[cfg(not(miri))] #[test] fn client_keep_alive_extra_body() { let _ = pretty_env_logger::try_init(); let server = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = server.local_addr().unwrap(); let rt = runtime(); let connector = DebugConnector::new(); let connects = connector.connects.clone(); let client = Client::builder(TokioExecutor::new()).build(connector); let (tx1, rx1) = oneshot::channel(); let (tx2, rx2) = 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"); sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello") .expect("write 1"); // the body "hello", while ignored because its a HEAD request, should mean the connection // cannot be put back in the pool let _ = tx1.send(()); let mut sock2 = server.accept().unwrap().0; let n2 = sock2.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); sock2 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") .expect("write 2"); let _ = tx2.send(()); }); assert_eq!(connects.load(Ordering::Relaxed), 0); let rx = rx1; let req = Request::builder() .method("HEAD") .uri(&*format!("http://{addr}/a")) .body(Empty::::new()) .unwrap(); let res = client.request(req); rt.block_on(future::join(res, rx).map(|r| r.0)).unwrap(); assert_eq!(connects.load(Ordering::Relaxed), 1); let rx = rx2; let req = Request::builder() .uri(&*format!("http://{addr}/b")) .body(Empty::::new()) .unwrap(); let res = client.request(req); rt.block_on(future::join(res, rx).map(|r| r.0)).unwrap(); assert_eq!(connects.load(Ordering::Relaxed), 2); } #[cfg(not(miri))] #[tokio::test] async fn client_keep_alive_when_response_before_request_body_ends() { let _ = pretty_env_logger::try_init(); let server = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = server.local_addr().unwrap(); let (closes_tx, mut closes) = mpsc::channel::<()>(10); let connector = DebugConnector::with_http_and_closes(HttpConnector::new(), closes_tx); let connects = connector.connects.clone(); let client = Client::builder(TokioExecutor::new()).build(connector.clone()); let (tx1, rx1) = oneshot::channel(); let (tx2, rx2) = oneshot::channel(); let (_tx3, rx3) = std::sync::mpsc::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"); sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") .expect("write 1"); // after writing the response, THEN stream the body let _ = tx1.send(()); sock.read(&mut buf).expect("read 2"); let _ = tx2.send(()); // prevent this thread from closing until end of test, so the connection // stays open and idle until Client is dropped let _ = rx3.recv(); }); assert_eq!(connects.load(Ordering::Relaxed), 0); let delayed_body = rx1 .then(|_| Box::pin(tokio::time::sleep(Duration::from_millis(200)))) .map(|_| Ok::<_, ()>(Frame::data(&b"hello a"[..]))) .map_err(|_| -> hyper::Error { panic!("rx1") }) .into_stream(); let req = Request::builder() .method("POST") .uri(&*format!("http://{addr}/a")) .body(StreamBody::new(delayed_body)) .unwrap(); let res = client.request(req).map_ok(move |res| { assert_eq!(res.status(), hyper::StatusCode::OK); }); future::join(res, rx2).await.0.unwrap(); std::future::poll_fn(|ctx| { assert!(Pin::new(&mut closes).poll_next(ctx).is_pending()); Poll::Ready(()) }) .await; assert_eq!(connects.load(Ordering::Relaxed), 1); drop(client); let t = tokio::time::sleep(Duration::from_millis(100)).map(|_| panic!("time out")); futures_util::pin_mut!(t); let close = closes.into_future().map(|(opt, _)| opt.expect("closes")); future::select(t, close).await; } #[cfg(not(miri))] #[tokio::test] async fn client_keep_alive_eager_when_chunked() { // If a response body has been read to completion, with completion // determined by some other factor, like decompression, and thus // it is in't polled a final time to clear the final 0-len chunk, // try to eagerly clear it so the connection can still be used. let _ = pretty_env_logger::try_init(); let server = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = server.local_addr().unwrap(); let connector = DebugConnector::new(); let connects = connector.connects.clone(); let client = Client::builder(TokioExecutor::new()).build(connector); let (tx1, rx1) = oneshot::channel(); let (tx2, rx2) = oneshot::channel(); thread::spawn(move || { let mut sock = server.accept().unwrap().0; //drop(server); 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"); sock.write_all( b"\ HTTP/1.1 200 OK\r\n\ transfer-encoding: chunked\r\n\ \r\n\ 5\r\n\ hello\r\n\ 0\r\n\r\n\ ", ) .expect("write 1"); let _ = tx1.send(()); let n2 = sock.read(&mut buf).expect("read 2"); assert_ne!(n2, 0, "bytes of second request"); 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(()); }); assert_eq!(connects.load(Ordering::SeqCst), 0); let rx = rx1; let req = Request::builder() .uri(&*format!("http://{addr}/a")) .body(Empty::::new()) .unwrap(); let fut = client.request(req); let resp = future::join(fut, rx).map(|r| r.0).await.unwrap(); assert_eq!(connects.load(Ordering::SeqCst), 1); assert_eq!(resp.status(), 200); assert_eq!(resp.headers()["transfer-encoding"], "chunked"); // Read the "hello" chunk... let chunk = resp.collect().await.unwrap().to_bytes(); assert_eq!(chunk, "hello"); // sleep real quick to let the threadpool put connection in ready // state and back into client pool tokio::time::sleep(Duration::from_millis(50)).await; let rx = rx2; let req = Request::builder() .uri(&*format!("http://{addr}/b")) .body(Empty::::new()) .unwrap(); let fut = client.request(req); future::join(fut, rx).map(|r| r.0).await.unwrap(); assert_eq!( connects.load(Ordering::SeqCst), 1, "second request should still only have 1 connect" ); drop(client); } #[cfg(not(miri))] #[test] fn connect_proxy_sends_absolute_uri() { let _ = pretty_env_logger::try_init(); let server = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = server.local_addr().unwrap(); let rt = runtime(); let connector = DebugConnector::new().proxy(); let client = Client::builder(TokioExecutor::new()).build(connector); let (tx1, rx1) = oneshot::channel(); thread::spawn(move || { let mut sock = server.accept().unwrap().0; //drop(server); 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]; let n = sock.read(&mut buf).expect("read 1"); let expected = format!("GET http://{addr}/foo/bar HTTP/1.1\r\nhost: {addr}\r\n\r\n"); assert_eq!(s(&buf[..n]), expected); sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") .expect("write 1"); let _ = tx1.send(()); }); let rx = rx1; let req = Request::builder() .uri(&*format!("http://{addr}/foo/bar")) .body(Empty::::new()) .unwrap(); let res = client.request(req); rt.block_on(future::join(res, rx).map(|r| r.0)).unwrap(); } #[cfg(not(miri))] #[test] fn connect_proxy_http_connect_sends_authority_form() { let _ = pretty_env_logger::try_init(); let server = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = server.local_addr().unwrap(); let rt = runtime(); let connector = DebugConnector::new().proxy(); let client = Client::builder(TokioExecutor::new()).build(connector); let (tx1, rx1) = oneshot::channel(); thread::spawn(move || { let mut sock = server.accept().unwrap().0; //drop(server); 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]; let n = sock.read(&mut buf).expect("read 1"); let expected = format!("CONNECT {addr} HTTP/1.1\r\nhost: {addr}\r\n\r\n"); assert_eq!(s(&buf[..n]), expected); sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") .expect("write 1"); let _ = tx1.send(()); }); let rx = rx1; let req = Request::builder() .method("CONNECT") .uri(&*format!("http://{addr}/useless/path")) .body(Empty::::new()) .unwrap(); let res = client.request(req); rt.block_on(future::join(res, rx).map(|r| r.0)).unwrap(); } #[cfg(not(miri))] #[test] fn client_upgrade() { use tokio::io::{AsyncReadExt, AsyncWriteExt}; let _ = pretty_env_logger::try_init(); let server = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = server.local_addr().unwrap(); let rt = runtime(); let connector = DebugConnector::new(); let client = Client::builder(TokioExecutor::new()).build(connector); 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"); sock.write_all( b"\ HTTP/1.1 101 Switching Protocols\r\n\ Upgrade: foobar\r\n\ \r\n\ foobar=ready\ ", ) .unwrap(); let _ = tx1.send(()); let n = sock.read(&mut buf).expect("read 2"); assert_eq!(&buf[..n], b"foo=bar"); sock.write_all(b"bar=foo").expect("write 2"); }); let rx = rx1; let req = Request::builder() .method("GET") .uri(&*format!("http://{addr}/up")) .body(Empty::::new()) .unwrap(); let res = client.request(req); let res = rt.block_on(future::join(res, rx).map(|r| r.0)).unwrap(); assert_eq!(res.status(), 101); let upgraded = rt.block_on(hyper::upgrade::on(res)).expect("on_upgrade"); let parts = upgraded.downcast::().unwrap(); assert_eq!(s(&parts.read_buf), "foobar=ready"); let mut io = parts.io; rt.block_on(io.write_all(b"foo=bar")).unwrap(); let mut vec = vec![]; rt.block_on(io.read_to_end(&mut vec)).unwrap(); assert_eq!(vec, b"bar=foo"); } #[cfg(not(miri))] #[test] fn client_http2_upgrade() { use http::{Method, Response, Version}; use hyper::service::service_fn; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpListener; let _ = pretty_env_logger::try_init(); let rt = runtime(); let server = rt .block_on(TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))) .unwrap(); let addr = server.local_addr().unwrap(); let mut connector = DebugConnector::new(); connector.alpn_h2 = true; let client = Client::builder(TokioExecutor::new()).build(connector); rt.spawn(async move { let (stream, _) = server.accept().await.expect("accept"); let stream = TokioIo::new(stream); let mut builder = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()); // IMPORTANT: This is required to advertise our support for HTTP/2 websockets to the client. builder.http2().enable_connect_protocol(); builder .serve_connection_with_upgrades( stream, service_fn(|req| async move { assert_eq!(req.headers().get("host"), None); assert_eq!(req.version(), Version::HTTP_2); assert_eq!( req.headers().get(http::header::SEC_WEBSOCKET_VERSION), Some(&http::header::HeaderValue::from_static("13")) ); assert_eq!( req.extensions().get::(), Some(&hyper::ext::Protocol::from_static("websocket")) ); let on_upgrade = hyper::upgrade::on(req); tokio::spawn(async move { let upgraded = on_upgrade.await.unwrap(); let mut io = TokioIo::new(upgraded); let mut vec = vec![]; io.read_buf(&mut vec).await.unwrap(); assert_eq!(vec, b"foo=bar"); io.write_all(b"bar=foo").await.unwrap(); }); Ok::<_, hyper::Error>(Response::new(Empty::::new())) }), ) .await .expect("server"); }); let req = Request::builder() .method(Method::CONNECT) .uri(&*format!("http://{addr}/up")) .header(http::header::SEC_WEBSOCKET_VERSION, "13") .version(Version::HTTP_2) .extension(hyper::ext::Protocol::from_static("websocket")) .body(Empty::::new()) .unwrap(); let res = client.request(req); let res = rt.block_on(res).unwrap(); assert_eq!(res.status(), http::StatusCode::OK); assert_eq!(res.version(), Version::HTTP_2); let upgraded = rt.block_on(hyper::upgrade::on(res)).expect("on_upgrade"); let mut io = TokioIo::new(upgraded); rt.block_on(io.write_all(b"foo=bar")).unwrap(); let mut vec = vec![]; rt.block_on(io.read_to_end(&mut vec)).unwrap(); assert_eq!(vec, b"bar=foo"); } #[cfg(not(miri))] #[test] fn alpn_h2() { use http::Response; use hyper::service::service_fn; use tokio::net::TcpListener; let _ = pretty_env_logger::try_init(); let rt = runtime(); let listener = rt .block_on(TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))) .unwrap(); let addr = listener.local_addr().unwrap(); let mut connector = DebugConnector::new(); connector.alpn_h2 = true; let connects = connector.connects.clone(); let client = Client::builder(TokioExecutor::new()).build(connector); rt.spawn(async move { let (stream, _) = listener.accept().await.expect("accept"); let stream = TokioIo::new(stream); hyper::server::conn::http2::Builder::new(TokioExecutor::new()) .serve_connection( stream, service_fn(|req| async move { assert_eq!(req.headers().get("host"), None); Ok::<_, hyper::Error>(Response::new(Full::::from("Hello, world"))) }), ) .await .expect("server"); }); assert_eq!(connects.load(Ordering::SeqCst), 0); let url = format!("http://{addr}/a").parse::<::hyper::Uri>().unwrap(); let res1 = client.get(url.clone()); let res2 = client.get(url.clone()); let res3 = client.get(url.clone()); rt.block_on(future::try_join3(res1, res2, res3)).unwrap(); // Since the client doesn't know it can ALPN at first, it will have // started 3 connections. But, the server above will only handle 1, // so the unwrapped responses futures show it still worked. assert_eq!(connects.load(Ordering::SeqCst), 3); let res4 = client.get(url.clone()); rt.block_on(res4).unwrap(); // HTTP/2 request allowed let res5 = client.request( Request::builder() .uri(url) .version(hyper::Version::HTTP_2) .body(Empty::::new()) .unwrap(), ); rt.block_on(res5).unwrap(); assert_eq!( connects.load(Ordering::SeqCst), 3, "after ALPN, no more connects" ); drop(client); } #[cfg(not(miri))] #[test] fn capture_connection_on_client() { let _ = pretty_env_logger::try_init(); let rt = runtime(); let connector = DebugConnector::new(); let client = Client::builder(TokioExecutor::new()).build(connector); let server = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = server.local_addr().unwrap(); 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"); sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") .expect("write 1"); }); let mut req = Request::builder() .uri(&*format!("http://{addr}/a")) .body(Empty::::new()) .unwrap(); let captured_conn = capture_connection(&mut req); rt.block_on(client.request(req)).expect("200 OK"); assert!(captured_conn.connection_metadata().is_some()); } #[cfg(not(miri))] #[test] fn connection_poisoning() { use std::sync::atomic::AtomicUsize; let _ = pretty_env_logger::try_init(); let rt = runtime(); let connector = DebugConnector::new(); let client = Client::builder(TokioExecutor::new()).build(connector); let server = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = server.local_addr().unwrap(); let num_conns: Arc = Default::default(); let num_requests: Arc = Default::default(); let num_requests_tracker = num_requests.clone(); let num_conns_tracker = num_conns.clone(); thread::spawn(move || loop { let mut sock = server.accept().unwrap().0; num_conns_tracker.fetch_add(1, Ordering::Relaxed); let num_requests_tracker = num_requests_tracker.clone(); thread::spawn(move || { 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]; loop { if sock.read(&mut buf).expect("read 1") > 0 { num_requests_tracker.fetch_add(1, Ordering::Relaxed); sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") .expect("write 1"); } } }); }); let make_request = || { Request::builder() .uri(&*format!("http://{addr}/a")) .body(Empty::::new()) .unwrap() }; let mut req = make_request(); let captured_conn = capture_connection(&mut req); rt.block_on(client.request(req)).expect("200 OK"); assert_eq!(num_conns.load(Ordering::SeqCst), 1); assert_eq!(num_requests.load(Ordering::SeqCst), 1); rt.block_on(client.request(make_request())).expect("200 OK"); rt.block_on(client.request(make_request())).expect("200 OK"); // Before poisoning the connection is reused assert_eq!(num_conns.load(Ordering::SeqCst), 1); assert_eq!(num_requests.load(Ordering::SeqCst), 3); captured_conn .connection_metadata() .as_ref() .unwrap() .poison(); rt.block_on(client.request(make_request())).expect("200 OK"); // After poisoning, a new connection is established assert_eq!(num_conns.load(Ordering::SeqCst), 2); assert_eq!(num_requests.load(Ordering::SeqCst), 4); rt.block_on(client.request(make_request())).expect("200 OK"); // another request can still reuse: assert_eq!(num_conns.load(Ordering::SeqCst), 2); assert_eq!(num_requests.load(Ordering::SeqCst), 5); } // ------------------------------------------------------- // Below is our custom code for testing hyper legacy-client behavior with mock connections for PR #184 // We use fully qualified paths for all types and identifiers to make this code // copy/paste-able without relying on external 'use' statements. Detailed inline // comments explain the purpose and logic of each section. //XXX: can manually run like this: // $ cargo test --features="http1,http2,server,client-legacy" --test legacy_client -- test_connection_error_propagation test_incomplete_message_error --nocapture // $ cargo test --all-features --test legacy_client -- --nocapture // $ cargo test --all-features --test legacy_client use std::error::Error; // needed for .source() eg. error[E0599]: no method named `source` found for struct `hyper_util::client::legacy::Error` in the current scope // Helper function to debug byte slices by attempting to interpret them as UTF-8. // If the bytes are valid UTF-8, they are printed as a string; otherwise, they are // printed as a raw byte array. This aids in debugging tokio_test::io::Mock mismatches. fn debug_bytes(bytes: &[u8], label: &str) { // Try to convert the byte slice to a UTF-8 string. // If successful, print it with the provided label for context. if let Ok(s) = std::str::from_utf8(bytes) { eprintln!("{}: {}", label, s); } else { // If the bytes are not valid UTF-8, print them as a raw byte array. eprintln!("{}: {:?}", label, bytes); } } // Struct representing a mock connection for testing hyper client behavior. // Implements hyper::rt::Read, hyper::rt::Write, and hyper_util::client::legacy::connect::Connection // traits to simulate I/O operations. Uses tokio_test::io::Mock for controlled I/O behavior. struct MockConnection { // The underlying mock I/O object, wrapped in hyper_util::rt::TokioIo for compatibility. inner: hyper_util::rt::TokioIo, // Atomic flag to signal a connection failure, controlling poll_read behavior. failed: std::sync::Arc, // The error to return when failed=true, simulating an I/O failure. error: std::sync::Arc, // Optional channel to signal unexpected writes, used for debugging. error_tx: Option>, // Tracks total bytes written, for logging and verification. bytes_written: usize, } impl MockConnection { // Constructor for MockConnection, initializing all fields. // Takes a mock I/O object, failure flag, error, and optional error channel. fn new( mock: tokio_test::io::Mock, failed: std::sync::Arc, error: std::sync::Arc, error_tx: Option>, ) -> Self { MockConnection { inner: hyper_util::rt::TokioIo::new(mock), failed, error, error_tx, bytes_written: 0, } } } // Implement hyper::rt::Read trait to handle read operations on the mock connection. // Controls whether an error or mock I/O data is returned based on the failed flag. impl hyper::rt::Read for MockConnection { // Polls the connection for reading, filling the provided buffer. // If failed=true, returns the stored error; otherwise, delegates to the mock I/O. fn poll_read( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, buf: hyper::rt::ReadBufCursor<'_>, ) -> std::task::Poll> { // Log the current state of the failed flag for debugging. eprintln!( "poll_read: failed={}", self.failed.load(std::sync::atomic::Ordering::SeqCst) ); // Check if the connection is marked as failed. // If true, return the stored error immediately to simulate a connection failure. if self.failed.load(std::sync::atomic::Ordering::SeqCst) { // Log the error being returned for traceability. eprintln!("poll_read: returning error: {}", self.error); // Create a new io::Error with the same kind and message as the stored error. return std::task::Poll::Ready(std::result::Result::Err(std::io::Error::new( self.error.kind(), self.error.to_string(), ))); } // If not failed, delegate to the mock I/O to simulate normal read behavior. // This may return EOF (Poll::Ready(Ok(0))) for empty IoBuilder. let inner = std::pin::Pin::new(&mut self.inner); inner.poll_read(cx, buf) } } // Implement hyper::rt::Write trait to handle write operations on the mock connection. // Logs writes and signals unexpected writes via error_tx. impl hyper::rt::Write for MockConnection { // Polls the connection for writing, sending the provided buffer. // Logs the write operation and tracks total bytes written. fn poll_write( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, buf: &[u8], ) -> std::task::Poll> { // Log the size of the buffer being written for debugging. eprintln!("poll_write: {} bytes", buf.len()); // Debug the buffer contents as UTF-8 or raw bytes. debug_bytes(buf, "poll_write buffer"); // Delegate the write to the mock I/O object. let inner = std::pin::Pin::new(&mut self.inner); match inner.poll_write(cx, buf) { // If the write succeeds, update the bytes_written counter and log the result. std::task::Poll::Ready(std::result::Result::Ok(bytes)) => { // Increment the total bytes written for tracking. self.bytes_written += bytes; // Log the number of bytes written and the running total. eprintln!( "poll_write: wrote {} bytes, total={}", bytes, self.bytes_written ); // If error_tx is present, signal an unexpected write (used in error tests). // This helps detect writes when the connection should fail early. if let Some(tx) = self.error_tx.take() { // Log that an unexpected write is being signaled. eprintln!("poll_write: signaling unexpected write"); // Send a message through the channel, ignoring errors if the receiver is closed. let _ = tx.try_send(()); } // Return the successful write result. std::task::Poll::Ready(std::result::Result::Ok(bytes)) } // For pending or error results, propagate them directly. other => other, } } // Polls the connection to flush any buffered data. // Delegates to the mock I/O object. fn poll_flush( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { // Log the flush operation for debugging. eprintln!("poll_flush"); // Delegate the flush to the mock I/O object. let inner = std::pin::Pin::new(&mut self.inner); inner.poll_flush(cx) } // Polls the connection to shut down the write side. // Delegates to the mock I/O object. fn poll_shutdown( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { // Log the shutdown operation for debugging. eprintln!("poll_shutdown"); // Delegate the shutdown to the mock I/O object. let inner = std::pin::Pin::new(&mut self.inner); inner.poll_shutdown(cx) } } // Implement hyper_util::client::legacy::connect::Connection trait to provide connection metadata. // Required for hyper to use MockConnection as a valid connection. impl hyper_util::client::legacy::connect::Connection for MockConnection { // Returns metadata about the connection. // In this case, a default Connected object indicating a new connection. fn connected(&self) -> hyper_util::client::legacy::connect::Connected { hyper_util::client::legacy::connect::Connected::new() } } // Struct representing a mock connector for creating MockConnection instances. // Implements tower_service::Service to integrate with hyper’s client. #[derive(Clone)] struct MockConnector { // The IoBuilder used to create mock I/O objects for each connection. io_builder: tokio_test::io::Builder, // Optional error to simulate a connection failure, passed to MockConnection. conn_error: Option>, } impl MockConnector { // Constructor for MockConnector, initializing the IoBuilder and optional error. fn new( io_builder: tokio_test::io::Builder, conn_error: Option>, ) -> Self { MockConnector { io_builder, conn_error, } } } // Implement tower_service::Service for MockConnector to create MockConnection instances. // Takes a hyper::Uri and returns a future resolving to a MockConnection. impl tower_service::Service for MockConnector { type Response = crate::MockConnection; type Error = std::io::Error; type Future = std::pin::Pin< Box< dyn std::future::Future> + Send, >, >; // Polls the connector to check if it’s ready to handle a request. // Always ready, as we don’t have resource constraints. fn poll_ready( &mut self, _cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { std::task::Poll::Ready(std::result::Result::Ok(())) } // Creates a new MockConnection for the given URI. // Configures the connection based on io_builder and conn_error. fn call(&mut self, _req: hyper::Uri) -> Self::Future { // Clone the IoBuilder to create a fresh mock I/O object. let mut io_builder = self.io_builder.clone(); // Clone the optional connection error for this call. let conn_error = self.conn_error.clone(); // Return a pinned future that creates the MockConnection. Box::pin(async move { // Build the mock I/O object from the IoBuilder. // This defines the I/O behavior (e.g., EOF for empty builder). let mock = io_builder.build(); // Create an atomic flag to track connection failure, initially false. let failed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); // Set the default error for non-failure cases. // Used when conn_error is None, simulating a clean EOF or connection close. let error = if let Some(ref err) = conn_error { err.clone() } else { std::sync::Arc::new(std::io::Error::new( std::io::ErrorKind::BrokenPipe, "connection closed", )) }; // Create an mpsc channel for signaling unexpected writes, if conn_error is set. // This helps debug cases where writes occur despite an expected failure. let error_tx = if conn_error.is_some() { // Create a channel with a buffer of 1 for signaling writes. let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(1); // Spawn a task to log unexpected writes when received. tokio::spawn(async move { // Wait for a message indicating a write occurred. if rx.recv().await.is_some() { // Log the unexpected write for debugging. eprintln!("Unexpected write occurred"); } }); Some(tx) } else { None }; // If a connection error is provided, mark the connection as failed. // This causes poll_read to return the error immediately. if let Some(err_clone) = conn_error { // Set the failed flag to true atomically. failed.store(true, std::sync::atomic::Ordering::SeqCst); // Log the simulated error for traceability. eprintln!("Simulated conn task error: {}", err_clone); } // Create and return the MockConnection with all configured components. std::result::Result::Ok(crate::MockConnection::new(mock, failed, error, error_tx)) }) } } // Test for connection error propagation with PR #184. // Simulates a connection failure by setting failed=true and returning a custom io::Error. // Verifies the error propagates through hyper’s client as a hyper::Error(Io, ...). #[tokio::test] async fn test_connection_error_propagation_pr184() { // Define the error message for the simulated connection failure. // Reused for creating the error and verifying the result. let err_str = "mock connection failure"; // Create an io::Error with Other kind and the custom message. // Wrapped in Arc for sharing across threads and MockConnection. let io_error = std::sync::Arc::new(std::io::Error::new(std::io::ErrorKind::Other, err_str)); // Create an empty IoBuilder, as no I/O is expected. // The error triggers before any reads or writes occur. let io_builder = tokio_test::io::Builder::new(); // Create a MockConnector with the error to simulate a failed connection. // The error will set failed=true in MockConnection. let connector = crate::MockConnector::new(io_builder, Some(io_error.clone())); // Build the hyper client with TokioExecutor and our connector. // pool_max_idle_per_host(0) disables connection pooling for a fresh connection. let client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) .pool_max_idle_per_host(0) .build::<_, http_body_util::Empty>(connector); // Build a GET request to a mock URI with custom headers. // Uses mixed-case headers to match your style, ensuring case-insensitive handling. let request = hyper::Request::builder() .uri("http://mocked") .header("hoSt", "mocked") .header("conNection", "close") .body(http_body_util::Empty::::new()) .expect("failed to build request"); // Send the request and capture the result. // Expect it to fail due to the simulated connection error. let result = client.request(request).await; // Extract the error, as the request should fail. let err = result.expect_err("expected request to fail"); // Log the full error for debugging, including its structure. // Matches your detailed logging style for traceability. eprintln!("Actually gotten error is: {:?}", err); // Downcast the error to a hyper::Error to verify its type. // Expect a hyper::Error wrapping an io::Error from MockConnection. let hyper_err = err .source() .and_then(|e| e.downcast_ref::()) .expect("expected hyper::Error"); // Downcast the hyper::Error’s source to an io::Error. // Verify it matches the simulated error from MockConnection. let io_err = hyper_err .source() .and_then(|e| e.downcast_ref::()) .expect(&format!("expected io::Error but got {:?}", hyper_err)); // Verify the io::Error has the expected kind (Other). assert_eq!(io_err.kind(), std::io::ErrorKind::Other); // Verify the io::Error’s message matches err_str. assert_eq!(io_err.to_string(), err_str); } // Test for consistent IncompleteMessage error with or without PR #184. // Simulates a connection that returns EOF immediately, causing hyper’s HTTP/1.1 parser // to fail with IncompleteMessage due to no response data. // Uses MockConnector with conn_error=None to keep failed=false, ensuring EOF behavior. #[tokio::test] async fn test_incomplete_message_error_pr184() { // Create an empty IoBuilder to simulate a connection with no data. // No write or read expectations, so poll_read returns EOF (Poll::Ready(Ok(0))). // This triggers IncompleteMessage in hyper’s parser. let io_builder = tokio_test::io::Builder::new(); // Create MockConnector with no error (conn_error=None). // Keeps failed=false in MockConnection, so poll_read delegates to the mock’s EOF. let connector = crate::MockConnector::new(io_builder, None); // Build the hyper client with TokioExecutor and our connector. // pool_max_idle_per_host(0) disables pooling for a fresh connection. let client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) .pool_max_idle_per_host(0) .build::<_, http_body_util::Empty>(connector); // Build a GET request to a mock URI with headers. // Uses mixed-case headers to match test_connection_error_propagation_pr184. // Empty body ensures focus on response parsing failure. let request = hyper::Request::builder() .uri("http://mocked") .header("hoSt", "mocked") .header("conNection", "close") .body(http_body_util::Empty::::new()) .expect("failed to build request"); // Send the request and capture the result. // Expect failure due to EOF causing IncompleteMessage. let result = client.request(request).await; // Extract the error, as the request should fail. // Without PR #184, expect ChannelClosed; with PR #184, expect IncompleteMessage. let err = result.expect_err("expected request to fail"); // Log the full error for debugging, matching your style. eprintln!("Actually gotten error is: {:?}", err); // Downcast to hyper::Error to verify the error type. // Expect IncompleteMessage (with PR #184) or ChannelClosed (without). let hyper_err = err .source() .and_then(|e| e.downcast_ref::()) .expect("expected hyper::Error"); // Verify the error is IncompleteMessage when PR #184 is applied. // This checks the parser’s failure due to EOF. assert!( hyper_err.is_incomplete_message(), "expected IncompleteMessage, got {:?}", hyper_err ); // Confirm no io::Error is present, as this is a parsing failure, not I/O. // Ensures we’re testing the correct error type. assert!( hyper_err .source() .and_then(|e| e.downcast_ref::()) .is_none(), "expected no io::Error, got {:?}", hyper_err ); } // Test for a successful HTTP/1.1 connection using a mock connector. // Simulates a server that accepts a request and responds with a 200 OK. // Verifies the client correctly sends the request and receives the response. #[tokio::test] async fn test_successful_connection() { // Define the expected server response: a valid HTTP/1.1 200 OK with no body. let response = b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"; // Define the expected client request, including headers and CRLF termination. // This ensures the client sends the correct request format. let expected_request = b"GET / HTTP/1.1\r\nhost: mocked\r\nconnection: close\r\n\r\n"; // Create an IoBuilder to simulate the server’s I/O behavior. // Expect the client to write the request and read the response. let mut io_builder = tokio_test::io::Builder::new(); // Configure the IoBuilder to expect the request and provide the response. io_builder.write(expected_request).read(response); // Finalize the IoBuilder for use in the connector. let io_builder = io_builder; // Create a MockConnector with no error (conn_error=None). // Ensures failed=false, allowing normal I/O operations. let connector = crate::MockConnector::new(io_builder, None); // Build the hyper client with TokioExecutor and our connector. // pool_max_idle_per_host(0) ensures a fresh connection. let client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) .pool_max_idle_per_host(0) .build::<_, http_body_util::Empty>(connector); // Build a GET request to a mock URI with headers. // Uses mixed-case headers to match your style and verify case-insensitive handling. let request = hyper::Request::builder() .uri("http://mocked") .header("hOst", "mocked") .header("coNnection", "close") .body(http_body_util::Empty::::new()) .expect("failed to build request"); // Send the request and capture the response. // Expect a successful response due to the configured IoBuilder. let response = client .request(request) .await .expect("request should succeed"); // Verify the response status is 200 OK. assert_eq!(response.status(), 200); }