pub use reqwest::{
header::{self, HeaderValue},
Client as ReqwestClient,
Error as ReqwestError,
StatusCode,
};
use {
chrono::{DateTime, Utc},
serde::{Deserialize, Serialize},
};
use tracing::{debug, info,};
use crate::RipestatClientError;
use ripestat_common::VERSION;
#[derive(Clone, Copy)]
pub struct RequestOptions {
pub(crate) max_retry_secs: u32,
pub(crate) def_retry_secs: u32,
pub(crate) max_retries: u16,
}
impl Default for RequestOptions {
fn default() -> Self {
Self {
max_retry_secs: 120,
def_retry_secs: 60,
max_retries: 1,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct HttpData {
pub content_length: Option<u64>,
pub content_type: Option<String>,
pub scheme: Option<String>,
pub host: String,
pub expires: Option<String>,
pub cache_control: Option<String>,
pub received: DateTime<Utc>,
pub status_code: u16,
pub location: Option<String>,
pub access_control_allow_origin: Option<String>,
pub strict_transport_security: Option<String>,
pub retry_after: Option<String>,
pub request_uri: Option<String>,
}
pub struct ReqwestClientConfig {
pub user_agent_suffix: String,
pub https_only: bool,
pub accept_invalid_host_names: bool,
pub accept_invalid_certificates: bool,
pub follow_redirects: bool,
pub host: Option<HeaderValue>,
pub origin: Option<HeaderValue>,
pub timeout_secs: u64,
}
impl Default for ReqwestClientConfig {
fn default() -> Self {
Self {
user_agent_suffix: "library".to_string(),
https_only: true,
accept_invalid_host_names: false,
accept_invalid_certificates: false,
follow_redirects: true,
host: None,
origin: None,
timeout_secs: 60,
}
}
}
pub fn create_reqwest_client(config: &ReqwestClientConfig) -> Result<ReqwestClient, ReqwestError> {
let default_headers = default_headers(config);
let mut client = reqwest::Client::builder();
let redirects = if config.follow_redirects {
reqwest::redirect::Policy::default()
} else {
reqwest::redirect::Policy::none()
};
client = client
.timeout(std::time::Duration::from_secs(config.timeout_secs))
.user_agent(format!(
"library {VERSION} {}",
config.user_agent_suffix
))
.redirect(redirects)
.https_only(config.https_only)
.danger_accept_invalid_hostnames(config.accept_invalid_host_names)
.danger_accept_invalid_certs(config.accept_invalid_certificates);
let client = client.default_headers(default_headers).build()?;
Ok(client)
}
fn default_headers(config: &ReqwestClientConfig) -> header::HeaderMap {
let mut default_headers = header::HeaderMap::new();
default_headers.insert(
header::ACCEPT,
HeaderValue::from_static("application/json"),
);
if let Some(host) = &config.host {
default_headers.insert(header::HOST, host.into());
};
if let Some(origin) = &config.origin {
default_headers.insert(header::ORIGIN, origin.into());
}
default_headers
}
#[derive(Default)]
pub struct ClientConfig {
client_config: ReqwestClientConfig,
request_options: RequestOptions,
}
pub struct Client {
pub(crate) reqwest_client: ReqwestClient,
pub(crate) request_options: RequestOptions,
}
impl Client {
pub fn new(reqwest_client: ReqwestClient, request_options: RequestOptions) -> Self {
Self {
reqwest_client,
request_options,
}
}
}
pub fn create_client(config: &ClientConfig) -> Result<Client, RipestatClientError> {
let client = create_reqwest_client(&config.client_config)?;
Ok(Client::new(client, config.request_options))
}
pub(crate) struct WrappedResponse {
#[allow(dead_code)]
pub(crate) http_data: HttpData,
pub(crate) text: String,
}
pub(crate) async fn wrapped_request(
request_uri: reqwest::Url,
client: &Client,
) -> Result<WrappedResponse, ReqwestError> {
#[allow(unused_mut)] let mut response = client.reqwest_client.get(request_uri.clone()).send().await?;
#[cfg(not(target_arch = "wasm32"))]
{
let mut tries: u16 = 0;
loop {
debug!("HTTP version: {:?}", response.version());
if !matches!(response.status(), StatusCode::TOO_MANY_REQUESTS) {
break;
}
let retry_after_header = response
.headers()
.get(header::RETRY_AFTER)
.map(|value| value.to_str().unwrap().to_string());
let retry_after = if let Some(rt) = retry_after_header {
info!("Server says too many requests and to retry-after '{rt}'.");
rt
} else {
info!("Server says too many requests but does not offer 'retry-after' value.");
client.request_options.def_retry_secs.to_string()
};
let mut wait_time_seconds = if let Ok(date) = DateTime::parse_from_rfc2822(&retry_after)
{
(date.with_timezone(&Utc) - Utc::now()).num_seconds() as u64
} else if let Ok(seconds) = retry_after.parse::<u64>() {
seconds
} else {
info!(
"Unable to parse retry-after header value. Using {}",
client.request_options.def_retry_secs
);
client.request_options.def_retry_secs.into()
};
if wait_time_seconds == 0 {
info!("Given {wait_time_seconds} for retry-after. Does not make sense.");
wait_time_seconds = client.request_options.def_retry_secs as u64;
}
if wait_time_seconds > client.request_options.max_retry_secs as u64 {
info!(
"Server is asking to wait longer than configured max of {}.",
client.request_options.max_retry_secs
);
wait_time_seconds = client.request_options.max_retry_secs as u64;
}
info!("Waiting {wait_time_seconds} seconds to retry.");
tokio::time::sleep(tokio::time::Duration::from_secs(wait_time_seconds + 1)).await;
tries += 1;
if tries > client.request_options.max_retries {
info!("Max query retries reached.");
break;
} else {
response = client.reqwest_client.get(request_uri.clone()).send().await?;
}
}
}
let response = response.error_for_status()?;
let content_type = response
.headers()
.get(header::CONTENT_TYPE)
.map(|value| value.to_str().unwrap().to_string());
let expires = response
.headers()
.get(header::EXPIRES)
.map(|value| value.to_str().unwrap().to_string());
let cache_control = response
.headers()
.get(header::CACHE_CONTROL)
.map(|value| value.to_str().unwrap().to_string());
let location = response
.headers()
.get(header::LOCATION)
.map(|value| value.to_str().unwrap().to_string());
let access_control_allow_origin = response
.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(|value| value.to_str().unwrap().to_string());
let strict_transport_security = response
.headers()
.get(header::STRICT_TRANSPORT_SECURITY)
.map(|value| value.to_str().unwrap().to_string());
let retry_after = response
.headers()
.get(header::RETRY_AFTER)
.map(|value| value.to_str().unwrap().to_string());
let content_length = response.content_length();
let status_code = response.status().as_u16();
let url = response.url().to_owned();
let text = response.text().await?;
let http_data = HttpData {
status_code: status_code,
content_length: content_length,
content_type: content_type,
scheme: Some(url.scheme().to_owned()),
host: url.host_str().expect("URL has no host. This shouldn't happen.").to_owned(),
expires: expires,
cache_control: cache_control,
received: Utc::now(),
location: location,
access_control_allow_origin: access_control_allow_origin,
strict_transport_security: strict_transport_security,
retry_after: retry_after,
request_uri: Some(String::from(request_uri)),
};
Ok(WrappedResponse { http_data, text })
}