use async_trait::async_trait;
use origin_domain::{AppError, Result};
use origin_http::{Headers, HttpClient, HttpRequest, HttpResponse};
use std::time::Duration;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const DEFAULT_MAX_RESPONSE_BYTES: u64 = 10 * 1024 * 1024;
#[derive(Debug, Clone)]
pub struct ReqwestHttpClient {
inner: reqwest::Client,
max_response_bytes: u64,
}
impl ReqwestHttpClient {
pub fn new(user_agent: impl AsRef<str>) -> Result<Self> {
Self::builder(user_agent).build()
}
pub fn builder(user_agent: impl AsRef<str>) -> ReqwestHttpClientBuilder {
ReqwestHttpClientBuilder {
user_agent: user_agent.as_ref().to_owned(),
timeout: DEFAULT_TIMEOUT,
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES,
}
}
}
#[derive(Debug, Clone)]
pub struct ReqwestHttpClientBuilder {
user_agent: String,
timeout: Duration,
connect_timeout: Duration,
max_response_bytes: u64,
}
impl ReqwestHttpClientBuilder {
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self {
self.connect_timeout = connect_timeout;
self
}
pub fn max_response_bytes(mut self, max_response_bytes: u64) -> Self {
self.max_response_bytes = max_response_bytes;
self
}
pub fn build(self) -> Result<ReqwestHttpClient> {
let inner = reqwest::Client::builder()
.user_agent(self.user_agent)
.timeout(self.timeout)
.connect_timeout(self.connect_timeout)
.build()
.map_err(|error| {
AppError::configuration(format!("cannot build http client: {error}"))
})?;
Ok(ReqwestHttpClient {
inner,
max_response_bytes: self.max_response_bytes,
})
}
}
#[async_trait]
impl HttpClient for ReqwestHttpClient {
async fn send(&self, request: HttpRequest) -> Result<HttpResponse> {
let method = reqwest::Method::from_bytes(request.method.as_str().as_bytes())
.map_err(|error| AppError::internal(format!("invalid http method: {error}")))?;
tracing::debug!(
method = %request.method,
url = %request.url.split('?').next().unwrap_or(&request.url),
"http request"
);
let mut builder = self.inner.request(method, &request.url);
for (name, value) in request.headers.iter() {
builder = builder.header(name, value);
}
if let Some(body) = request.body {
builder = builder.body(body);
}
let response = builder.send().await.map_err(to_app_error)?;
let status = response.status().as_u16();
let headers = response
.headers()
.iter()
.filter_map(|(name, value)| {
value
.to_str()
.ok()
.map(|value| (name.as_str().to_owned(), value.to_owned()))
})
.collect::<Headers>();
let body = read_body_limited(response, self.max_response_bytes).await?;
tracing::debug!(status, bytes = body.len(), "http response");
Ok(HttpResponse::new(status, headers, body))
}
}
async fn read_body_limited(mut response: reqwest::Response, limit: u64) -> Result<Vec<u8>> {
if let Some(length) = response.content_length()
&& length > limit
{
return Err(AppError::ExternalService(format!(
"response declared {length} bytes, over the {limit} byte limit"
)));
}
let mut body = Vec::new();
while let Some(chunk) = response.chunk().await.map_err(to_app_error)? {
if body.len() as u64 + chunk.len() as u64 > limit {
return Err(AppError::ExternalService(format!(
"response body exceeds the {limit} byte limit"
)));
}
body.extend_from_slice(&chunk);
}
Ok(body)
}
fn to_app_error(error: reqwest::Error) -> AppError {
if error.is_timeout() {
return AppError::Network(format!("request timed out: {error}"));
}
if error.is_connect() {
return AppError::Offline(format!("cannot reach host: {error}"));
}
AppError::Network(error.to_string())
}