#![cfg(not(target_arch = "wasm32"))] #![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))] mod support; use http::Response; use http::Uri; use hyper_util::rt::TokioIo; use std::net::SocketAddr; use std::sync::{Arc, RwLock}; use std::task::{Context, Poll}; use support::server; use tokio::net::TcpStream; use tower_service::Service; use http::header::{CONTENT_LENGTH, CONTENT_TYPE, TRANSFER_ENCODING}; #[cfg(feature = "json")] use std::collections::HashMap; use cf_reqwest::{BoxConn, Client, GenericConnectionFut}; #[tokio::test] async fn auto_headers() { let server = server::http(move |req| async move { assert_eq!(req.method(), "GET"); assert_eq!(req.headers()["accept"], "*/*"); assert_eq!(req.headers().get("user-agent"), None); if cfg!(feature = "gzip") { assert!(req.headers()["accept-encoding"] .to_str() .unwrap() .contains("gzip")); } if cfg!(feature = "brotli") { assert!(req.headers()["accept-encoding"] .to_str() .unwrap() .contains("br")); } if cfg!(feature = "zstd") { assert!(req.headers()["accept-encoding"] .to_str() .unwrap() .contains("zstd")); } if cfg!(feature = "deflate") { assert!(req.headers()["accept-encoding"] .to_str() .unwrap() .contains("deflate")); } http::Response::default() }); let url = format!("http://{}/1", server.addr()); let res = cf_reqwest::Client::builder() .no_proxy() .build() .unwrap() .get(&url) .send() .await .unwrap(); assert_eq!(res.url().as_str(), &url); assert_eq!(res.status(), cf_reqwest::StatusCode::OK); assert_eq!(res.remote_addr(), Some(server.addr())); } #[tokio::test] async fn donot_set_content_length_0_if_have_no_body() { let server = server::http(move |req| async move { let headers = req.headers(); assert_eq!(headers.get(CONTENT_LENGTH), None); assert!(headers.get(CONTENT_TYPE).is_none()); assert!(headers.get(TRANSFER_ENCODING).is_none()); dbg!(&headers); http::Response::default() }); let url = format!("http://{}/content-length", server.addr()); let res = cf_reqwest::Client::builder() .no_proxy() .build() .expect("client builder") .get(&url) .send() .await .expect("request"); assert_eq!(res.status(), cf_reqwest::StatusCode::OK); } #[cfg(feature = "http3")] #[tokio::test] async fn http3_request_full() { use http_body_util::BodyExt; let server = server::http3(move |req| async move { assert_eq!(req.headers()[CONTENT_LENGTH], "5"); let reqb = req.collect().await.unwrap().to_bytes(); assert_eq!(reqb, "hello"); http::Response::default() }); let url = format!("https://{}/content-length", server.addr()); let res = cf_reqwest::Client::builder() .http3_prior_knowledge() .danger_accept_invalid_certs(true) .build() .expect("client builder") .post(url) .version(http::Version::HTTP_3) .body("hello") .send() .await .expect("request"); assert_eq!(res.version(), http::Version::HTTP_3); assert_eq!(res.status(), cf_reqwest::StatusCode::OK); } #[tokio::test] async fn user_agent() { let server = server::http(move |req| async move { assert_eq!(req.headers()["user-agent"], "reqwest-test-agent"); http::Response::default() }); let url = format!("http://{}/ua", server.addr()); let res = cf_reqwest::Client::builder() .user_agent("reqwest-test-agent") .build() .expect("client builder") .get(&url) .send() .await .expect("request"); assert_eq!(res.status(), cf_reqwest::StatusCode::OK); } #[tokio::test] async fn enforce_plaintext() { let server = server::http(move |_req| async { http::Response::new("Hello".into()) }); let url = format!("http://{}/text", server.addr()); let res = cf_reqwest::Client::builder() .enforce_plaintext(true) .build() .expect("client builder") .get(&url) .send() .await .expect("request"); let text = res.text().await.expect("Failed to get text"); assert_eq!("Hello", text); let url = format!("https://{}/text", server.addr()); let res = cf_reqwest::Client::builder() .enforce_plaintext(true) .build() .expect("client builder") .get(&url) .send() .await .expect("request"); let text = res.text().await.expect("Failed to get text"); assert_eq!("Hello", text); } #[tokio::test] async fn response_text() { let _ = env_logger::try_init(); let server = server::http(move |_req| async { http::Response::new("Hello".into()) }); let client = Client::new(); let res = client .get(&format!("http://{}/text", server.addr())) .send() .await .expect("Failed to get"); assert_eq!(res.content_length(), Some(5)); let text = res.text().await.expect("Failed to get text"); assert_eq!("Hello", text); } #[tokio::test] async fn response_bytes() { let _ = env_logger::try_init(); let server = server::http(move |_req| async { http::Response::new("Hello".into()) }); let client = Client::new(); let res = client .get(&format!("http://{}/bytes", server.addr())) .send() .await .expect("Failed to get"); assert_eq!(res.content_length(), Some(5)); let bytes = res.bytes().await.expect("res.bytes()"); assert_eq!("Hello", bytes); } #[tokio::test] #[cfg(feature = "json")] async fn response_json() { let _ = env_logger::try_init(); let server = server::http(move |_req| async { http::Response::new("\"Hello\"".into()) }); let client = Client::new(); let res = client .get(&format!("http://{}/json", server.addr())) .send() .await .expect("Failed to get"); let text = res.json::().await.expect("Failed to get json"); assert_eq!("Hello", text); } #[tokio::test] async fn body_pipe_response() { use http_body_util::BodyExt; let _ = env_logger::try_init(); let server = server::http(move |req| async move { if req.uri() == "/get" { http::Response::new("pipe me".into()) } else { assert_eq!(req.uri(), "/pipe"); assert_eq!(req.headers()["transfer-encoding"], "chunked"); let full: Vec = req .into_body() .collect() .await .expect("must succeed") .to_bytes() .to_vec(); assert_eq!(full, b"pipe me"); http::Response::default() } }); let client = Client::new(); let res1 = client .get(&format!("http://{}/get", server.addr())) .send() .await .expect("get1"); assert_eq!(res1.status(), cf_reqwest::StatusCode::OK); assert_eq!(res1.content_length(), Some(7)); // and now ensure we can "pipe" the response to another request let res2 = client .post(&format!("http://{}/pipe", server.addr())) .body(res1) .send() .await .expect("res2"); assert_eq!(res2.status(), cf_reqwest::StatusCode::OK); } #[tokio::test] async fn overridden_dns_resolution_with_gai() { let _ = env_logger::builder().is_test(true).try_init(); let server = server::http(move |_req| async { http::Response::new("Hello".into()) }); let overridden_domain = "rust-lang.org"; let url = format!( "http://{overridden_domain}:{}/domain_override", server.addr().port() ); let client = cf_reqwest::Client::builder() .no_proxy() .resolve(overridden_domain, server.addr()) .build() .expect("client builder"); let req = client.get(&url); let res = req.send().await.expect("request"); assert_eq!(res.status(), cf_reqwest::StatusCode::OK); let text = res.text().await.expect("Failed to get text"); assert_eq!("Hello", text); } #[tokio::test] async fn overridden_dns_resolution_with_gai_multiple() { let _ = env_logger::builder().is_test(true).try_init(); let server = server::http(move |_req| async { http::Response::new("Hello".into()) }); let overridden_domain = "rust-lang.org"; let url = format!( "http://{overridden_domain}:{}/domain_override", server.addr().port() ); // the server runs on IPv4 localhost, so provide both IPv4 and IPv6 and let the happy eyeballs // algorithm decide which address to use. let client = cf_reqwest::Client::builder() .no_proxy() .resolve_to_addrs( overridden_domain, &[ std::net::SocketAddr::new( std::net::IpAddr::V6(std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), server.addr().port(), ), server.addr(), ], ) .build() .expect("client builder"); let req = client.get(&url); let res = req.send().await.expect("request"); assert_eq!(res.status(), cf_reqwest::StatusCode::OK); let text = res.text().await.expect("Failed to get text"); assert_eq!("Hello", text); } #[cfg(feature = "hickory-dns")] #[tokio::test] async fn overridden_dns_resolution_with_hickory_dns() { let _ = env_logger::builder().is_test(true).try_init(); let server = server::http(move |_req| async { http::Response::new("Hello".into()) }); let overridden_domain = "rust-lang.org"; let url = format!( "http://{overridden_domain}:{}/domain_override", server.addr().port() ); let client = cf_reqwest::Client::builder() .no_proxy() .resolve(overridden_domain, server.addr()) .hickory_dns(true) .build() .expect("client builder"); let req = client.get(&url); let res = req.send().await.expect("request"); assert_eq!(res.status(), cf_reqwest::StatusCode::OK); let text = res.text().await.expect("Failed to get text"); assert_eq!("Hello", text); } #[cfg(feature = "hickory-dns")] #[tokio::test] async fn overridden_dns_resolution_with_hickory_dns_multiple() { let _ = env_logger::builder().is_test(true).try_init(); let server = server::http(move |_req| async { http::Response::new("Hello".into()) }); let overridden_domain = "rust-lang.org"; let url = format!( "http://{overridden_domain}:{}/domain_override", server.addr().port() ); // the server runs on IPv4 localhost, so provide both IPv4 and IPv6 and let the happy eyeballs // algorithm decide which address to use. let client = cf_reqwest::Client::builder() .no_proxy() .resolve_to_addrs( overridden_domain, &[ std::net::SocketAddr::new( std::net::IpAddr::V6(std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), server.addr().port(), ), server.addr(), ], ) .hickory_dns(true) .build() .expect("client builder"); let req = client.get(&url); let res = req.send().await.expect("request"); assert_eq!(res.status(), cf_reqwest::StatusCode::OK); let text = res.text().await.expect("Failed to get text"); assert_eq!("Hello", text); } #[cfg(any(feature = "native-tls", feature = "__rustls",))] #[test] fn use_preconfigured_tls_with_bogus_backend() { struct DefinitelyNotTls; cf_reqwest::Client::builder() .use_preconfigured_tls(DefinitelyNotTls) .build() .expect_err("definitely is not TLS"); } #[cfg(feature = "native-tls")] #[test] fn use_preconfigured_native_tls_default() { extern crate native_tls_crate; let tls = native_tls_crate::TlsConnector::builder() .build() .expect("tls builder"); cf_reqwest::Client::builder() .use_preconfigured_tls(tls) .build() .expect("preconfigured default tls"); } #[cfg(feature = "__rustls")] #[test] fn use_preconfigured_rustls_default() { extern crate rustls; let root_cert_store = rustls::RootCertStore::empty(); let tls = rustls::ClientConfig::builder() .with_root_certificates(root_cert_store) .with_no_client_auth(); cf_reqwest::Client::builder() .use_preconfigured_tls(tls) .build() .expect("preconfigured rustls tls"); } #[cfg(feature = "__rustls")] #[tokio::test] #[ignore = "Needs TLS support in the test server"] async fn http2_upgrade() { let server = server::http(move |_| async move { http::Response::default() }); let url = format!("https://localhost:{}", server.addr().port()); let res = cf_reqwest::Client::builder() .danger_accept_invalid_certs(true) .use_rustls_tls() .build() .expect("client builder") .get(&url) .send() .await .expect("request"); assert_eq!(res.status(), cf_reqwest::StatusCode::OK); assert_eq!(res.version(), cf_reqwest::Version::HTTP_2); } #[cfg(feature = "default-tls")] #[cfg_attr(feature = "http3", ignore = "enabling http3 seems to break this, why?")] #[tokio::test] async fn test_allowed_methods() { let resp = cf_reqwest::Client::builder() .https_only(true) .build() .expect("client builder") .get("https://google.com") .send() .await; assert!(resp.is_ok()); let resp = cf_reqwest::Client::builder() .https_only(true) .build() .expect("client builder") .get("http://google.com") .send() .await; assert!(resp.is_err()); } #[test] #[cfg(feature = "json")] fn add_json_default_content_type_if_not_set_manually() { let mut map = HashMap::new(); map.insert("body", "json"); let content_type = http::HeaderValue::from_static("application/vnd.api+json"); let req = Client::new() .post("https://google.com/") .header(CONTENT_TYPE, &content_type) .json(&map) .build() .expect("request is not valid"); assert_eq!(content_type, req.headers().get(CONTENT_TYPE).unwrap()); } #[test] #[cfg(feature = "json")] fn update_json_content_type_if_set_manually() { let mut map = HashMap::new(); map.insert("body", "json"); let req = Client::new() .post("https://google.com/") .json(&map) .build() .expect("request is not valid"); assert_eq!("application/json", req.headers().get(CONTENT_TYPE).unwrap()); } #[tokio::test] async fn custom_connector() { let server = server::http(|_| async move { Response::builder().body("Hello world".into()).unwrap() }); struct CustomConnector(SocketAddr); impl Service for CustomConnector { type Response = BoxConn; type Error = Box; type Future = GenericConnectionFut; fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, _req: Uri) -> Self::Future { let addr = self.0; Box::pin(async move { Ok(Box::new(TokioIo::new(TcpStream::connect(addr).await.unwrap())) as Box<_>) }) } } let res = cf_reqwest::Client::builder() .connector(Arc::new(RwLock::new(CustomConnector(server.addr())))) .build() .expect("client builder") .get("http://google.com") .send() .await .expect("request"); assert_eq!(res.text().await.unwrap(), "Hello world"); } #[cfg(all(feature = "__tls", not(feature = "rustls-tls-manual-roots")))] #[tokio::test] async fn test_tls_info() { let resp = cf_reqwest::Client::builder() .tls_info(true) .build() .expect("client builder") .get("https://google.com") .send() .await .expect("response"); let tls_info = resp.extensions().get::(); assert!(tls_info.is_some()); let tls_info = tls_info.unwrap(); let peer_certificate = tls_info.peer_certificate(); assert!(peer_certificate.is_some()); let der = peer_certificate.unwrap(); assert_eq!(der[0], 0x30); // ASN.1 SEQUENCE let resp = cf_reqwest::Client::builder() .build() .expect("client builder") .get("https://google.com") .send() .await .expect("response"); let tls_info = resp.extensions().get::(); assert!(tls_info.is_none()); } // NOTE: using the default "current_thread" runtime here would cause the test to // fail, because the only thread would block until `panic_rx` receives a // notification while the client needs to be driven to get the graceful shutdown // done. #[cfg(feature = "http2")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn highly_concurrent_requests_to_http2_server_with_low_max_concurrent_streams() { let client = cf_reqwest::Client::builder() .http2_prior_knowledge() .build() .unwrap(); let server = server::http_with_config( move |req| async move { assert_eq!(req.version(), http::Version::HTTP_2); http::Response::default() }, |builder| { builder.http2().max_concurrent_streams(1); }, ); let url = format!("http://{}", server.addr()); let futs = (0..100).map(|_| { let client = client.clone(); let url = url.clone(); async move { let res = client.get(&url).send().await.unwrap(); assert_eq!(res.status(), cf_reqwest::StatusCode::OK); } }); futures_util::future::join_all(futs).await; } #[cfg(feature = "http2")] #[tokio::test] async fn highly_concurrent_requests_to_slow_http2_server_with_low_max_concurrent_streams() { use support::delay_server; let client = cf_reqwest::Client::builder() .http2_prior_knowledge() .build() .unwrap(); let server = delay_server::Server::new( move |req| async move { assert_eq!(req.version(), http::Version::HTTP_2); http::Response::default() }, |http| { http.http2().max_concurrent_streams(1); }, std::time::Duration::from_secs(2), ) .await; let url = format!("http://{}", server.addr()); let futs = (0..100).map(|_| { let client = client.clone(); let url = url.clone(); async move { let res = client.get(&url).send().await.unwrap(); assert_eq!(res.status(), cf_reqwest::StatusCode::OK); } }); futures_util::future::join_all(futs).await; server.shutdown().await; }