use std::time::Duration;
use http_body_util::{BodyExt, Empty};
use hyper::body::Bytes;
use hyper_util::rt::TokioExecutor;
use crate::Result;
use crate::error::HttpError;
#[derive(Debug)]
pub struct HttpClientConfig {
pub user_agent: String,
pub timeout: Duration,
}
impl HttpClientConfig {
pub fn new(app_name: &str, version: &str) -> Self {
Self {
user_agent: format!("{app_name}/{version}"),
timeout: Duration::from_secs(30),
}
}
#[must_use]
pub const fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn with_user_agent(mut self, user_agent: &str) -> Self {
self.user_agent = user_agent.to_string();
self
}
}
type HttpsConnector =
hyper_rustls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>;
pub struct HttpClient {
inner: hyper_util::client::legacy::Client<HttpsConnector, Empty<Bytes>>,
config: HttpClientConfig,
}
impl HttpClient {
pub fn new(config: HttpClientConfig) -> Result<Self> {
let https = hyper_rustls::HttpsConnectorBuilder::new()
.with_provider_and_webpki_roots(rustls::crypto::ring::default_provider())
.map_err(HttpError::Tls)?
.https_or_http()
.enable_all_versions()
.build();
let inner = hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https);
Ok(Self { inner, config })
}
pub fn from_app(app_name: &str, version: &str) -> Result<Self> {
Self::new(HttpClientConfig::new(app_name, version))
}
#[tracing::instrument(skip(self), fields(url = %url))]
pub async fn get(&self, url: &str) -> Result<Response> {
let uri: hyper::Uri = url.parse().map_err(HttpError::InvalidUrl)?;
let req = hyper::Request::builder()
.method(hyper::Method::GET)
.uri(&uri)
.header(hyper::header::USER_AGENT, &self.config.user_agent)
.body(Empty::<Bytes>::new())
.map_err(HttpError::RequestBuild)?;
let whole_request = async {
let resp = self.inner.request(req).await.map_err(HttpError::Request)?;
let status = resp.status().as_u16();
tracing::debug!(status, "response received");
let body = resp
.into_body()
.collect()
.await
.map_err(HttpError::Body)?
.to_bytes();
Ok(Response {
status,
body: body.to_vec(),
})
};
tokio::time::timeout(self.config.timeout, whole_request)
.await
.map_err(|_| {
HttpError::Io(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("request timed out after {:?}", self.config.timeout),
))
})?
}
pub const fn config(&self) -> &HttpClientConfig {
&self.config
}
}
#[derive(Debug)]
pub struct Response {
pub status: u16,
body: Vec<u8>,
}
impl Response {
pub fn text(&self) -> std::result::Result<String, std::string::FromUtf8Error> {
String::from_utf8(self.body.clone())
}
pub fn into_text(self) -> std::result::Result<String, std::string::FromUtf8Error> {
String::from_utf8(self.body)
}
pub fn text_ref(&self) -> std::result::Result<&str, std::str::Utf8Error> {
std::str::from_utf8(&self.body)
}
pub fn json<T: serde::de::DeserializeOwned>(&self) -> crate::Result<T> {
serde_json::from_slice(&self.body).map_err(|e| HttpError::Json(e).into())
}
pub fn bytes(&self) -> &[u8] {
&self.body
}
pub const fn is_success(&self) -> bool {
self.status >= 200 && self.status < 300
}
}