use snafu::ResultExt;
pub mod mtls;
use huskarl_core::BoxedError;
use huskarl_core::http::{HttpClient, HttpResponse};
use bytes::Bytes;
use http::{HeaderMap, Request, StatusCode};
use snafu::Snafu;
#[derive(Clone)]
pub struct ReqwestClient {
client: reqwest::Client,
uses_mtls: bool,
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "rustls-tls", feature = "native-tls")
))]
identity: Option<reqwest::Identity>,
}
#[derive(Debug, Snafu)]
pub enum ReqwestBuilderError {
#[snafu(display("Failed to build HTTP client"))]
Build { source: reqwest::Error },
#[snafu(display("Failed to configure mTLS"))]
Mtls { source: BoxedError },
}
impl huskarl_core::Error for ReqwestBuilderError {
fn is_retryable(&self) -> bool {
false
}
}
impl From<reqwest::Client> for ReqwestClient {
fn from(client: reqwest::Client) -> Self {
Self {
client,
uses_mtls: false,
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "rustls-tls", feature = "native-tls")
))]
identity: None,
}
}
}
#[bon::bon]
impl ReqwestClient {
#[builder]
pub async fn new(
#[builder(required, into, default = Some(concat!("huskarl/", env!("CARGO_PKG_VERSION")).to_string()))]
user_agent: Option<String>,
mtls: impl mtls::MtlsProvider,
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "rustls-tls", feature = "native-tls")
))]
root_certificates: Option<Vec<reqwest::Certificate>>,
configure_builder: Option<
Box<dyn FnOnce(reqwest::ClientBuilder) -> reqwest::ClientBuilder>,
>,
) -> Result<Self, ReqwestBuilderError> {
let mut reqwest_builder = reqwest::Client::builder();
if let Some(user_agent) = user_agent {
reqwest_builder = reqwest_builder.user_agent(user_agent)
}
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "rustls-tls", feature = "native-tls")
))]
if let Some(root_certificates) = root_certificates {
reqwest_builder = reqwest_builder.tls_certs_only(root_certificates);
}
if let Some(configure_builder) = configure_builder {
reqwest_builder = configure_builder(reqwest_builder);
}
let uses_mtls = mtls.uses_mtls();
let mtls_output =
mtls.apply(reqwest_builder)
.await
.map_err(|e| ReqwestBuilderError::Mtls {
source: BoxedError::from_err(e),
})?;
Ok(Self {
client: mtls_output.builder.build().context(BuildSnafu)?,
uses_mtls,
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "rustls-tls", feature = "native-tls")
))]
identity: mtls_output.identity,
})
}
}
impl ReqwestClient {
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "rustls-tls", feature = "native-tls")
))]
pub fn identity(&self) -> Option<&reqwest::Identity> {
self.identity.as_ref()
}
}
#[derive(Debug)]
pub struct ReqwestResponse(reqwest::Response);
impl AsRef<reqwest::Response> for ReqwestResponse {
fn as_ref(&self) -> &reqwest::Response {
&self.0
}
}
#[derive(Debug, Snafu)]
#[snafu(transparent)]
pub struct ReqwestError {
source: reqwest::Error,
}
impl AsRef<reqwest::Error> for ReqwestError {
fn as_ref(&self) -> &reqwest::Error {
&self.source
}
}
impl HttpClient for ReqwestClient {
type Response = ReqwestResponse;
type Error = ReqwestError;
type ResponseError = <Self::Response as HttpResponse>::Error;
fn uses_mtls(&self) -> bool {
self.uses_mtls
}
async fn execute(&self, request: Request<Bytes>) -> Result<Self::Response, Self::Error> {
let (parts, body) = request.into_parts();
let reqwest_request = self
.client
.request(parts.method, parts.uri.to_string())
.headers(parts.headers)
.body(body)
.build()?;
Ok(self
.client
.execute(reqwest_request)
.await
.map(ReqwestResponse)?)
}
}
impl HttpResponse for ReqwestResponse {
type Error = ReqwestError;
fn status(&self) -> StatusCode {
self.0.status()
}
fn headers(&self) -> HeaderMap {
self.0.headers().clone()
}
async fn body(self) -> Result<Bytes, Self::Error> {
Ok(self.0.bytes().await?)
}
}
impl huskarl_core::Error for ReqwestError {
fn is_retryable(&self) -> bool {
#[cfg(not(target_arch = "wasm32"))]
{
self.source.is_connect()
}
#[cfg(target_arch = "wasm32")]
{
false
}
}
}