use std::time::Duration;
use http::HeaderValue;
use crate::Result;
pub use http::HeaderMap;
pub use http::header;
mod reqwest;
mod ureq;
#[cfg(feature = "async")]
pub use reqwest::ReqwestAsyncClient;
#[cfg(feature = "reqwest")]
pub use reqwest::ReqwestClient;
#[cfg(feature = "ureq")]
pub use ureq::UreqClient;
pub trait HttpClient: Send + Sync {
fn get(
&self,
url: &str,
headers: &HeaderMap,
timeout: Option<Duration>,
) -> Result<Box<dyn HttpResponse>>;
}
pub trait HttpResponse {
fn headers(&self) -> &HeaderMap<HeaderValue>;
fn body(self: Box<Self>) -> Box<dyn std::io::Read>;
fn body_buffered(self: Box<Self>) -> Box<dyn std::io::BufRead> {
Box::new(std::io::BufReader::new(self.body()))
}
}
#[cfg(feature = "async")]
pub trait AsyncHttpClient: Send + Sync {
fn get<'a>(
&'a self,
url: &'a str,
headers: &'a HeaderMap,
timeout: Option<Duration>,
) -> futures_util::future::BoxFuture<'a, Result<Box<dyn AsyncHttpResponse>>>;
}
#[cfg(feature = "async")]
pub trait AsyncHttpResponse: Send {
fn headers(&self) -> &HeaderMap<HeaderValue>;
fn text(self: Box<Self>) -> futures_util::future::BoxFuture<'static, Result<String>>;
fn bytes_stream(
self: Box<Self>,
) -> futures_util::stream::BoxStream<'static, Result<bytes::Bytes>>;
}
#[cfg(feature = "reqwest")]
pub(crate) fn default_client() -> Box<dyn HttpClient> {
#[cfg(test)]
DEFAULT_CLIENT_BACKEND.with(|c| c.set("reqwest"));
Box::new(ReqwestClient::default())
}
#[cfg(all(not(feature = "reqwest"), feature = "ureq"))]
pub(crate) fn default_client() -> Box<dyn HttpClient> {
#[cfg(test)]
DEFAULT_CLIENT_BACKEND.with(|c| c.set("ureq"));
Box::new(UreqClient::default())
}
pub(crate) type ClientBuildError = Box<dyn std::error::Error + Send + Sync>;
pub(crate) fn client_with_root_certs(
certs: &[crate::tls::Certificate],
) -> std::result::Result<std::sync::Arc<dyn HttpClient>, ClientBuildError> {
#[cfg(feature = "reqwest")]
{
crate::http_client::ReqwestClient::build_with_certs(certs)
}
#[cfg(all(feature = "ureq", not(feature = "reqwest")))]
{
crate::http_client::UreqClient::build_with_certs(certs)
}
#[cfg(not(any(feature = "reqwest", feature = "ureq")))]
{
let _ = certs;
Err("no HTTP client feature enabled".into())
}
}
#[cfg(feature = "async")]
pub(crate) fn async_client_with_root_certs(
certs: &[crate::tls::Certificate],
) -> std::result::Result<std::sync::Arc<dyn AsyncHttpClient>, ClientBuildError> {
crate::http_client::ReqwestAsyncClient::build_async_with_certs(certs)
}
#[cfg(test)]
thread_local! {
pub(crate) static DEFAULT_CLIENT_BACKEND: std::cell::Cell<&'static str> =
const { std::cell::Cell::new("unset") };
}
#[cfg(not(any(feature = "reqwest", feature = "ureq")))]
compile_error!(
"no HTTP client selected - enable at least one of the `reqwest` (default) or `ureq` features"
);
#[cfg(feature = "async")]
pub(crate) fn default_async_client() -> Box<dyn AsyncHttpClient> {
Box::new(ReqwestAsyncClient::default())
}
#[cfg(test)]
mod tests {
#[cfg(all(feature = "reqwest", feature = "ureq"))]
#[test]
fn default_client_prefers_reqwest_when_both_enabled() {
super::DEFAULT_CLIENT_BACKEND.with(|c| c.set("unset"));
let _client: Box<dyn super::HttpClient> = super::default_client();
let backend = super::DEFAULT_CLIENT_BACKEND.with(|c| c.get());
assert_eq!(
backend, "reqwest",
"with both clients enabled default_client() must instantiate the reqwest backend, not ureq"
);
assert_ne!(
backend, "ureq",
"the default must NOT be the ureq client when reqwest is also enabled"
);
}
}