use std::time::Duration;
use reqwest::{header, Method, Response, StatusCode};
use serde::de::DeserializeOwned;
use serde::Serialize;
use tokio::time::sleep;
use crate::error::{Result, ScreenshotFreeAPIError};
pub(crate) struct HttpClient {
inner: reqwest::Client,
base_url: String,
api_key: String,
}
#[derive(serde::Deserialize, Debug)]
struct ApiError {
#[serde(default)]
message: String,
#[serde(default)]
error: String,
#[serde(default)]
code: Option<String>,
}
impl ApiError {
fn text(&self) -> String {
if !self.message.is_empty() {
self.message.clone()
} else if !self.error.is_empty() {
self.error.clone()
} else {
"no message".to_owned()
}
}
}
impl HttpClient {
pub fn new(api_key: String, base_url: String) -> Self {
let inner = reqwest::Client::builder()
.timeout(Duration::from_secs(90))
.user_agent(concat!("screenshotfreeapi-rs/", env!("CARGO_PKG_VERSION")))
.build()
.expect("failed to build reqwest client");
Self { inner, base_url, api_key }
}
pub async fn get<T: DeserializeOwned>(&self, path: &str, jwt: Option<&str>) -> Result<T> {
self.dispatch(Method::GET, path, None::<&()>, jwt).await
}
pub async fn post<B: Serialize + Send + Sync, T: DeserializeOwned>(
&self,
path: &str,
body: &B,
jwt: Option<&str>,
) -> Result<T> {
self.dispatch(Method::POST, path, Some(body), jwt).await
}
pub async fn put<B: Serialize + Send + Sync, T: DeserializeOwned>(
&self,
path: &str,
body: &B,
jwt: Option<&str>,
) -> Result<T> {
self.dispatch(Method::PUT, path, Some(body), jwt).await
}
pub async fn delete<T: DeserializeOwned>(&self, path: &str, jwt: Option<&str>) -> Result<T> {
self.dispatch(Method::DELETE, path, None::<&()>, jwt).await
}
pub async fn patch<B: Serialize + Send + Sync, T: DeserializeOwned>(
&self,
path: &str,
body: &B,
jwt: Option<&str>,
) -> Result<T> {
self.dispatch(Method::PATCH, path, Some(body), jwt).await
}
async fn dispatch<B: Serialize + Send + Sync, T: DeserializeOwned>(
&self,
method: Method,
path: &str,
body: Option<&B>,
jwt: Option<&str>,
) -> Result<T> {
let url = format!("{}{}", self.base_url, path);
let auth_value = match jwt {
Some(token) => format!("Bearer {}", token),
None => format!("Bearer {}", self.api_key),
};
const MAX_RETRIES: u32 = 3;
let backoff_ms: [u64; 3] = [1_000, 2_000, 4_000];
let mut last_err: Option<ScreenshotFreeAPIError> = None;
for attempt in 0..=MAX_RETRIES {
let mut req = self
.inner
.request(method.clone(), &url)
.header(header::AUTHORIZATION, &auth_value)
.header(header::ACCEPT, "application/json");
if let Some(b) = body {
req = req.json(b);
}
let response = match req.send().await {
Ok(r) => r,
Err(e) => {
if attempt < MAX_RETRIES {
sleep(Duration::from_millis(backoff_ms[attempt as usize])).await;
last_err = Some(ScreenshotFreeAPIError::Http(e));
continue;
}
return Err(ScreenshotFreeAPIError::Http(e));
}
};
let status = response.status();
if status.is_success() {
return response.json::<T>().await.map_err(ScreenshotFreeAPIError::Http);
}
if status == StatusCode::TOO_MANY_REQUESTS {
let retry_after = Self::retry_after_secs(&response);
let body_text = response.text().await.unwrap_or_default();
if body_text.contains("quota") || body_text.contains("Quota") {
return Err(ScreenshotFreeAPIError::QuotaExceeded);
}
if attempt < MAX_RETRIES {
sleep(Duration::from_secs(retry_after)).await;
last_err = Some(ScreenshotFreeAPIError::RateLimit { retry_after_seconds: retry_after });
continue;
}
return Err(ScreenshotFreeAPIError::RateLimit { retry_after_seconds: retry_after });
}
if status.is_server_error() && attempt < MAX_RETRIES {
sleep(Duration::from_millis(backoff_ms[attempt as usize])).await;
last_err = Some(ScreenshotFreeAPIError::Unexpected {
status: status.as_u16(),
message: "server error".into(),
});
continue;
}
return Err(Self::map_error_response(status, response).await);
}
Err(last_err.unwrap_or_else(|| ScreenshotFreeAPIError::Unexpected {
status: 0,
message: "exceeded retry budget".into(),
}))
}
fn retry_after_secs(resp: &Response) -> u64 {
resp.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(5)
}
async fn map_error_response(status: StatusCode, resp: Response) -> ScreenshotFreeAPIError {
let body_text = resp.text().await.unwrap_or_default();
let api_err: ApiError = serde_json::from_str(&body_text).unwrap_or(ApiError {
message: body_text.clone(),
error: String::new(),
code: None,
});
let msg = api_err.text();
match status.as_u16() {
400 => ScreenshotFreeAPIError::Validation { message: msg },
401 => ScreenshotFreeAPIError::Authentication { message: msg },
402 => ScreenshotFreeAPIError::PaymentRequired { message: msg },
403 => ScreenshotFreeAPIError::Forbidden { message: msg },
404 => ScreenshotFreeAPIError::NotFound { message: msg },
_ => ScreenshotFreeAPIError::Unexpected { status: status.as_u16(), message: msg },
}
}
}