#[cfg(not(feature = "blocking"))]
use governor::{
Quota, RateLimiter,
clock::MonotonicClock,
middleware::NoOpMiddleware,
state::{InMemoryState, NotKeyed},
};
#[cfg(feature = "blocking")]
use reqwest::blocking::{Client, RequestBuilder, Response};
#[cfg(not(feature = "blocking"))]
use reqwest::{Client, RequestBuilder, Response};
use reqwest::{Method, Url};
use reqwest::{StatusCode, header::USER_AGENT};
use std::{env, fmt};
#[cfg(not(feature = "blocking"))]
use std::{num::NonZeroU32, sync::Arc, time::Duration};
use crate::{Error, Result, error::types::ErrorResponse};
#[cfg(doc)]
use crate::Resend;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ConfigBuilder {
api_key: String,
base_url: Option<Url>,
client: Option<Client>,
}
impl ConfigBuilder {
pub fn new<S>(api_key: S) -> Self
where
S: Into<String>,
{
Self {
api_key: api_key.into(),
base_url: None,
client: None,
}
}
#[must_use]
pub fn base_url(mut self, url: Url) -> Self {
self.base_url = Some(url);
self
}
#[must_use]
pub fn client(mut self, client: Client) -> Self {
self.client = Some(client);
self
}
pub fn build(self) -> Config {
Config::new(self.api_key, self.client.unwrap_or_default(), self.base_url)
}
}
#[non_exhaustive]
#[derive(Clone)]
pub struct Config {
pub(crate) user_agent: String,
pub(crate) api_key: String,
pub(crate) base_url: Url,
pub(crate) client: Client,
#[cfg(not(feature = "blocking"))]
limiter: Arc<
RateLimiter<
NotKeyed,
InMemoryState,
MonotonicClock,
NoOpMiddleware<<MonotonicClock as governor::clock::Clock>::Instant>,
>,
>,
}
impl Config {
pub fn builder<S>(api_key: S) -> ConfigBuilder
where
S: Into<String>,
{
ConfigBuilder::new(api_key.into())
}
#[must_use]
pub(crate) fn new(api_key: String, client: Client, base_url: Option<Url>) -> Self {
let env_base_url = base_url.unwrap_or_else(|| {
env::var("RESEND_BASE_URL")
.map_or_else(
|_| Url::parse("https://api.resend.com"),
|env_var| Url::parse(env_var.as_str()),
)
.expect("env variable `RESEND_BASE_URL` should be a valid URL")
});
let env_user_agent = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
#[cfg(not(feature = "blocking"))]
let rate_limit_per_sec = env::var("RESEND_RATE_LIMIT")
.unwrap_or_else(|_| "9".to_owned())
.parse::<u32>()
.expect("env variable `RESEND_RATE_LIMIT` should be a valid u32");
#[cfg(not(feature = "blocking"))]
let quota = Quota::with_period(Duration::from_millis(1100))
.expect("Valid quota")
.allow_burst(
NonZeroU32::new(rate_limit_per_sec).expect("Rate limit is a valid non zero u32"),
);
#[cfg(not(feature = "blocking"))]
let limiter = Arc::new(RateLimiter::direct_with_clock(quota, MonotonicClock));
Self {
user_agent: env_user_agent,
api_key,
base_url: env_base_url,
client,
#[cfg(not(feature = "blocking"))]
limiter,
}
}
pub(crate) fn build(&self, method: Method, path: &str) -> RequestBuilder {
let path = self
.base_url
.join(path)
.expect("should be a valid API endpoint");
self.client
.request(method, path)
.bearer_auth(self.api_key.as_str())
.header(USER_AGENT, self.user_agent.as_str())
}
#[allow(unreachable_pub)]
#[maybe_async::maybe_async]
pub async fn send(&self, request: RequestBuilder) -> Result<Response> {
#[cfg(not(feature = "blocking"))]
{
let jitter =
governor::Jitter::new(Duration::from_millis(10), Duration::from_millis(50));
self.limiter.until_ready_with_jitter(jitter).await;
}
let request = request.build()?;
let response = self.client.execute(request).await?;
match response.status() {
StatusCode::TOO_MANY_REQUESTS => {
let headers = response.headers();
let ratelimit_limit = headers
.get("ratelimit-limit")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok());
let ratelimit_remaining = headers
.get("ratelimit-remaining")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok());
let ratelimit_reset = headers
.get("ratelimit-reset")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok());
Err(Error::RateLimit {
ratelimit_limit,
ratelimit_remaining,
ratelimit_reset,
})
}
x if x.is_client_error() || x.is_server_error() => {
let content_type_is_html = response
.headers()
.get("content-type")
.and_then(|el| el.to_str().ok())
.is_some_and(|content_type| content_type.contains("html"));
if content_type_is_html {
return Err(Error::Parse {
message: response.text().await?,
source: None,
});
}
let error_raw = response.text().await?;
let error = serde_json::from_str::<ErrorResponse>(&error_raw).map_err(|e| {
Error::Parse {
message: error_raw,
source: Some(Box::new(e)),
}
})?;
Err(Error::Resend(error))
}
_ => Ok(response),
}
}
}
impl fmt::Debug for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Client")
.field("api_key", &"re_*********")
.field("user_agent", &self.user_agent.as_str())
.field("base_url", &self.base_url.as_str())
.finish_non_exhaustive()
}
}