use std::marker::PhantomData;
use std::time::Duration;
use bytes::Bytes;
use http::{HeaderMap, Method, StatusCode};
use crate::error::Error;
use crate::runtime::RuntimeCompletion;
use crate::traits::{HttpClient, RequestBuilderExt, ResponseExt};
pub struct BlockingClient<C: HttpClient, R: RuntimeCompletion> {
inner: C,
_runtime: PhantomData<R>,
}
impl<C: HttpClient, R: RuntimeCompletion> Clone for BlockingClient<C, R> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
_runtime: PhantomData,
}
}
}
impl<C: HttpClient, R: RuntimeCompletion> BlockingClient<C, R> {
pub fn new(inner: C) -> Self {
Self {
inner,
_runtime: PhantomData,
}
}
pub fn inner(&self) -> &C {
&self.inner
}
pub fn get(&self, uri: &str) -> Result<BlockingRequestBuilder<C, R>, Error> {
Ok(BlockingRequestBuilder {
inner: self.inner.get(uri)?,
_runtime: PhantomData,
})
}
pub fn head(&self, uri: &str) -> Result<BlockingRequestBuilder<C, R>, Error> {
Ok(BlockingRequestBuilder {
inner: self.inner.head(uri)?,
_runtime: PhantomData,
})
}
pub fn post(&self, uri: &str) -> Result<BlockingRequestBuilder<C, R>, Error> {
Ok(BlockingRequestBuilder {
inner: self.inner.post(uri)?,
_runtime: PhantomData,
})
}
pub fn put(&self, uri: &str) -> Result<BlockingRequestBuilder<C, R>, Error> {
Ok(BlockingRequestBuilder {
inner: self.inner.put(uri)?,
_runtime: PhantomData,
})
}
pub fn patch(&self, uri: &str) -> Result<BlockingRequestBuilder<C, R>, Error> {
Ok(BlockingRequestBuilder {
inner: self.inner.patch(uri)?,
_runtime: PhantomData,
})
}
pub fn delete(&self, uri: &str) -> Result<BlockingRequestBuilder<C, R>, Error> {
Ok(BlockingRequestBuilder {
inner: self.inner.delete(uri)?,
_runtime: PhantomData,
})
}
pub fn request(
&self,
method: Method,
uri: &str,
) -> Result<BlockingRequestBuilder<C, R>, Error> {
Ok(BlockingRequestBuilder {
inner: self.inner.request(method, uri)?,
_runtime: PhantomData,
})
}
}
pub struct BlockingRequestBuilder<C: HttpClient, R: RuntimeCompletion> {
inner: C::RequestBuilder,
_runtime: PhantomData<R>,
}
impl<C: HttpClient, R: RuntimeCompletion> BlockingRequestBuilder<C, R> {
pub fn header(
mut self,
name: http::header::HeaderName,
value: http::header::HeaderValue,
) -> Self {
self.inner = self.inner.header(name, value);
self
}
pub fn headers(mut self, headers: HeaderMap) -> Self {
self.inner = self.inner.headers(headers);
self
}
pub fn bearer_auth(mut self, token: &str) -> Self {
self.inner = self.inner.bearer_auth(token);
self
}
pub fn body(mut self, body: impl Into<Bytes>) -> Self {
self.inner = self.inner.body(body);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.inner = self.inner.timeout(timeout);
self
}
pub fn send(
self,
) -> Result<BlockingResponse<<C::RequestBuilder as RequestBuilderExt>::Response, R>, Error>
{
let resp = R::block_on(self.inner.send())?.map_err(|e| e.into_error())?;
Ok(BlockingResponse {
inner: resp,
_runtime: PhantomData,
})
}
}
pub struct BlockingResponse<Resp: ResponseExt, R: RuntimeCompletion> {
inner: Resp,
_runtime: PhantomData<R>,
}
impl<Resp: ResponseExt, R: RuntimeCompletion> BlockingResponse<Resp, R> {
pub fn status(&self) -> StatusCode {
self.inner.status()
}
pub fn headers(&self) -> &HeaderMap {
self.inner.headers()
}
pub fn content_length(&self) -> Option<u64> {
self.inner
.headers()
.get(http::header::CONTENT_LENGTH)?
.to_str()
.ok()?
.parse()
.ok()
}
pub fn error_for_status(self) -> Result<Self, Error> {
let status = self.inner.status();
if status.is_client_error() || status.is_server_error() {
Err(Error::Status(status))
} else {
Ok(self)
}
}
pub fn error_for_status_ref(&self) -> Result<&Self, Error> {
let status = self.inner.status();
if status.is_client_error() || status.is_server_error() {
Err(Error::Status(status))
} else {
Ok(self)
}
}
pub fn bytes(self) -> Result<Bytes, Error> {
R::block_on(self.inner.bytes())?
}
pub fn text(self) -> Result<String, Error> {
R::block_on(self.inner.text())?
}
}
impl<Resp: ResponseExt, R: RuntimeCompletion> std::fmt::Debug for BlockingResponse<Resp, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BlockingResponse")
.field("status", &self.inner.status())
.finish_non_exhaustive()
}
}
#[cfg(not(target_arch = "wasm32"))]
impl<R: RuntimeCompletion> BlockingResponse<crate::Response, R> {
pub fn url(&self) -> &http::Uri {
self.inner.url()
}
pub fn remote_addr(&self) -> Option<std::net::SocketAddr> {
self.inner.remote_addr()
}
pub fn version(&self) -> http::Version {
self.inner.version()
}
pub fn tls_info(&self) -> Option<&crate::tls::TlsInfo> {
self.inner.tls_info()
}
#[cfg(feature = "json")]
pub fn json<T: serde::de::DeserializeOwned>(self) -> Result<T, Error> {
R::block_on(self.inner.json())?
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "tokio")]
#[test]
fn blocking_tokio_default_headers() {
use crate::runtime::tokio_rt::TcpConnector;
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::builder()
.user_agent("blocking-test/1.0")
.build()
.unwrap();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let result = client.get("http://127.0.0.1:1/nonexistent");
assert!(result.is_ok());
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_client_all_methods() {
use crate::runtime::tokio_rt::TcpConnector;
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
assert!(client.get("http://127.0.0.1:1/").is_ok());
assert!(client.head("http://127.0.0.1:1/").is_ok());
assert!(client.post("http://127.0.0.1:1/").is_ok());
assert!(client.put("http://127.0.0.1:1/").is_ok());
assert!(client.patch("http://127.0.0.1:1/").is_ok());
assert!(client.delete("http://127.0.0.1:1/").is_ok());
assert!(
client
.request(Method::OPTIONS, "http://127.0.0.1:1/")
.is_ok()
);
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_client_invalid_url() {
use crate::runtime::tokio_rt::TcpConnector;
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
assert!(client.get("not a valid url\n").is_err());
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_client_invalid_url_all_methods() {
use crate::runtime::tokio_rt::TcpConnector;
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let bad_url = "not a valid url\n";
assert!(client.head(bad_url).is_err());
assert!(client.post(bad_url).is_err());
assert!(client.put(bad_url).is_err());
assert!(client.patch(bad_url).is_err());
assert!(client.delete(bad_url).is_err());
assert!(client.request(Method::OPTIONS, bad_url).is_err());
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_response_error_for_status_ok() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server();
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.get(&format!("http://{}/", addr))
.unwrap()
.send()
.unwrap();
assert!(resp.error_for_status().is_ok());
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_response_error_for_status_ref_ok() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server();
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.get(&format!("http://{}/", addr))
.unwrap()
.send()
.unwrap();
assert!(resp.error_for_status_ref().is_ok());
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_response_debug() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server();
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.get(&format!("http://{}/", addr))
.unwrap()
.send()
.unwrap();
let dbg = format!("{:?}", resp);
assert!(dbg.contains("BlockingResponse"));
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_response_bytes_and_text() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server();
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.get(&format!("http://{}/", addr))
.unwrap()
.send()
.unwrap();
let body = resp.bytes().unwrap();
assert!(!body.is_empty());
let resp2 = client
.get(&format!("http://{}/", addr))
.unwrap()
.send()
.unwrap();
let text = resp2.text().unwrap();
assert!(!text.is_empty());
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_response_url_and_version() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server();
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.get(&format!("http://{}/", addr))
.unwrap()
.send()
.unwrap();
assert!(resp.url().to_string().contains(&addr.to_string()));
assert_eq!(resp.version(), http::Version::HTTP_11);
assert!(resp.remote_addr().is_some());
assert!(resp.tls_info().is_none());
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_head_sends_and_returns_status() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server_with(aioduct_test_server::h1::echo);
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.head(&format!("http://{}/test", addr))
.unwrap()
.send()
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_post_sends_body_and_returns_echo() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server_with(aioduct_test_server::h1::echo);
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.post(&format!("http://{}/submit", addr))
.unwrap()
.body("payload")
.send()
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let text = resp.text().unwrap();
assert!(
text.contains("method=POST"),
"expected POST method in echo: {text}"
);
assert!(
text.contains("body=payload"),
"expected body in echo: {text}"
);
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_put_sends_and_echoes_method() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server_with(aioduct_test_server::h1::echo);
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.put(&format!("http://{}/resource", addr))
.unwrap()
.body("update")
.send()
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let text = resp.text().unwrap();
assert!(
text.contains("method=PUT"),
"expected PUT method in echo: {text}"
);
assert!(
text.contains("body=update"),
"expected body in echo: {text}"
);
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_patch_sends_and_echoes_method() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server_with(aioduct_test_server::h1::echo);
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.patch(&format!("http://{}/resource", addr))
.unwrap()
.body("partial")
.send()
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let text = resp.text().unwrap();
assert!(
text.contains("method=PATCH"),
"expected PATCH method in echo: {text}"
);
assert!(
text.contains("body=partial"),
"expected body in echo: {text}"
);
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_delete_sends_and_echoes_method() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server_with(aioduct_test_server::h1::echo);
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.delete(&format!("http://{}/resource/42", addr))
.unwrap()
.send()
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let text = resp.text().unwrap();
assert!(
text.contains("method=DELETE"),
"expected DELETE method in echo: {text}"
);
assert!(
text.contains("path=/resource/42"),
"expected path in echo: {text}"
);
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_custom_method_sends_and_echoes() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server_with(aioduct_test_server::h1::echo);
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.request(Method::OPTIONS, &format!("http://{}/options", addr))
.unwrap()
.send()
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let text = resp.text().unwrap();
assert!(
text.contains("method=OPTIONS"),
"expected OPTIONS method in echo: {text}"
);
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_response_error_for_status_4xx() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server_with(|_req| async {
Ok(hyper::Response::builder()
.status(404)
.body(http_body_util::Full::new(bytes::Bytes::from("not found")))
.unwrap())
});
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.get(&format!("http://{}/missing", addr))
.unwrap()
.send()
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let err = resp.error_for_status().unwrap_err();
match err {
crate::error::Error::Status(s) => assert_eq!(s, StatusCode::NOT_FOUND),
other => panic!("expected Error::Status, got: {other:?}"),
}
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_response_error_for_status_5xx() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server_with(|_req| async {
Ok(hyper::Response::builder()
.status(500)
.body(http_body_util::Full::new(bytes::Bytes::from(
"internal error",
)))
.unwrap())
});
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.get(&format!("http://{}/fail", addr))
.unwrap()
.send()
.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
let err = resp.error_for_status().unwrap_err();
match err {
crate::error::Error::Status(s) => assert_eq!(s, StatusCode::INTERNAL_SERVER_ERROR),
other => panic!("expected Error::Status, got: {other:?}"),
}
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_response_error_for_status_ref_4xx() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server_with(|_req| async {
Ok(hyper::Response::builder()
.status(403)
.body(http_body_util::Full::new(bytes::Bytes::from("forbidden")))
.unwrap())
});
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.get(&format!("http://{}/secret", addr))
.unwrap()
.send()
.unwrap();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
let err = resp.error_for_status_ref().unwrap_err();
match err {
crate::error::Error::Status(s) => assert_eq!(s, StatusCode::FORBIDDEN),
other => panic!("expected Error::Status, got: {other:?}"),
}
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_response_content_length_present() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server_with(|_req| async {
Ok(hyper::Response::builder()
.header("content-length", "7")
.body(http_body_util::Full::new(bytes::Bytes::from("1234567")))
.unwrap())
});
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.get(&format!("http://{}/", addr))
.unwrap()
.send()
.unwrap();
assert_eq!(resp.content_length(), Some(7));
}
#[cfg(feature = "tokio")]
#[test]
fn blocking_json_deserialization() {
use crate::runtime::tokio_rt::TcpConnector;
let addr = aioduct_test_server::h1::spawn_h1_server_with(|_req| async {
let body = r#"{"name":"aioduct","version":1}"#;
Ok(hyper::Response::builder()
.header("content-type", "application/json")
.body(http_body_util::Full::new(bytes::Bytes::from(body)))
.unwrap())
});
let engine = crate::HttpEngineSend::<crate::runtime::TokioRuntime, TcpConnector>::new();
let client = BlockingClient::<_, crate::runtime::TokioRuntime>::new(engine);
let resp = client
.get(&format!("http://{}/", addr))
.unwrap()
.send()
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
#[derive(serde::Deserialize, Debug)]
struct Info {
name: String,
version: u32,
}
let info: Info = resp.json().unwrap();
assert_eq!(info.name, "aioduct");
assert_eq!(info.version, 1);
}
}