pub use reqwest;
use crate::{entry::Entry, sinks::message::Message};
use once_cell::sync::OnceCell;
use reqwest::Client;
use std::{convert::identity, fmt::Debug, time::Duration};
use url::Url;
use super::Fetch;
const USER_AGENT: &str =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96.0";
pub(crate) static CLIENT: OnceCell<reqwest::Client> = OnceCell::new();
pub use serde_json::Value as Json;
pub struct Http {
pub url: Url,
request: Request,
client: reqwest::Client,
}
#[expect(missing_docs, reason = "error message is self-documenting")]
#[derive(thiserror::Error, Debug)]
pub enum HttpError {
#[error("Invalid JSON for the POST request")]
BadJson(#[from] serde_json::Error),
#[error("Failed to init TLS")]
TlsInitFailed(#[source] reqwest::Error),
#[error("Can't send an HTTP request to {1:?}")]
BadRequest(#[source] reqwest::Error, String),
#[error("Not a valid URL")]
InvalidUrl(#[from] url::ParseError),
}
#[derive(Debug)]
pub enum Request {
Get,
Post(Json),
}
impl Http {
pub fn new_get(url: impl TryInto<Url, Error = url::ParseError>) -> Result<Self, HttpError> {
Self::new_with_client_config(url.try_into()?, Request::Get, identity)
}
pub fn new_post(
url: impl TryInto<Url, Error = url::ParseError>,
body: &str,
) -> Result<Self, HttpError> {
Self::new_with_client_config(
url.try_into()?,
Request::Post(serde_json::from_str(body)?),
identity,
)
}
pub fn new_with_client_config<F>(
url: Url,
request: Request,
builder_config: F,
) -> Result<Self, HttpError>
where
F: FnOnce(reqwest::ClientBuilder) -> reqwest::ClientBuilder,
{
let client = CLIENT
.get_or_try_init(|| {
let builder = reqwest::ClientBuilder::new().timeout(Duration::from_secs(30));
builder_config(builder)
.build()
.map_err(HttpError::TlsInitFailed)
})?
.clone();
Ok(Self {
url,
request,
client,
})
}
}
impl Fetch for Http {
type Err = HttpError;
#[tracing::instrument(skip_all)]
async fn fetch(&mut self) -> Result<Vec<Entry>, Self::Err> {
tracing::debug!("Sending an HTTP request");
let page = send_request(&self.client, &self.request, &self.url).await?;
let entry = Entry::builder()
.raw_contents(page)
.msg(Message::builder().link(self.url.as_str().to_owned()))
.build();
Ok(vec![entry])
}
}
pub(crate) async fn send_request(
client: &Client,
request: &Request,
url: &Url,
) -> Result<String, HttpError> {
let request = match request {
Request::Get => {
tracing::trace!("Making an HTTP GET request to {:?}", url.as_str());
client.get(url.as_str())
}
Request::Post(json) => {
tracing::trace!(
"Making an HTTP POST request to {:?} with {:#?}",
url.as_str(),
json
);
client.post(url.as_str()).json(json)
}
};
let response = request
.header(reqwest::header::USER_AGENT, USER_AGENT)
.send()
.await
.map_err(|e| HttpError::BadRequest(e, url.to_string()))?;
tracing::trace!("Getting text body of the response");
response
.text()
.await
.map_err(|e| HttpError::BadRequest(e, url.to_string()))
}
impl Debug for Http {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Http")
.field("url", &self.url.as_str())
.field("request", &self.request)
.finish_non_exhaustive()
}
}