use std::{borrow::Cow, fmt, sync::Arc, time::Duration};
use bytes::Bytes;
use futures_util::stream::BoxStream;
use http::{HeaderMap, StatusCode};
use url::Url;
use crate::{
cookies::CookieJar,
request::{Method, Request, RequestBody},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("HTTP status {status}")]
pub struct HttpStatusError {
pub status: StatusCode,
pub retry_after: Option<Duration>,
}
impl HttpStatusError {
pub fn new(status: StatusCode) -> Self {
Self {
status,
retry_after: None,
}
}
pub fn with_retry_after(mut self, retry_after: Duration) -> Self {
self.retry_after = Some(retry_after);
self
}
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum HttpClientError {
#[error("failed to build HTTP client: {0}")]
Build(#[source] anyhow::Error),
#[error("invalid HTTP request: {0}")]
InvalidRequest(#[source] anyhow::Error),
#[error("HTTP connection failed: {0}")]
Connect(#[source] anyhow::Error),
#[error("HTTP request timed out: {0}")]
Timeout(#[source] anyhow::Error),
#[error("HTTP redirect failed: {0}")]
Redirect(#[source] anyhow::Error),
#[error("HTTP response decode failed: {0}")]
Decode(#[source] anyhow::Error),
#[error("HTTP I/O failed: {0}")]
Io(#[source] anyhow::Error),
#[error("HTTP client error: {0}")]
Other(#[source] anyhow::Error),
}
impl HttpClientError {
pub fn is_connect(&self) -> bool {
matches!(self, Self::Connect(_))
}
pub fn is_timeout(&self) -> bool {
matches!(self, Self::Timeout(_))
}
pub fn build(error: impl Into<anyhow::Error>) -> Self {
Self::Build(error.into())
}
pub fn invalid_request(error: impl Into<anyhow::Error>) -> Self {
Self::InvalidRequest(error.into())
}
pub fn connect(error: impl Into<anyhow::Error>) -> Self {
Self::Connect(error.into())
}
pub fn timeout(error: impl Into<anyhow::Error>) -> Self {
Self::Timeout(error.into())
}
pub fn redirect(error: impl Into<anyhow::Error>) -> Self {
Self::Redirect(error.into())
}
pub fn decode(error: impl Into<anyhow::Error>) -> Self {
Self::Decode(error.into())
}
pub fn io(error: impl Into<anyhow::Error>) -> Self {
Self::Io(error.into())
}
pub fn other(error: impl Into<anyhow::Error>) -> Self {
Self::Other(error.into())
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct HttpRequest {
pub url: Url,
pub method: Method,
pub headers: HeaderMap,
pub body: Option<RequestBody>,
pub cookie_jar: Option<Arc<CookieJar>>,
pub proxy: Option<Url>,
pub timeout: Option<Duration>,
pub max_redirects: u32,
pub use_header_generator: bool,
pub session_token: Option<crate::session::SessionToken>,
}
impl HttpRequest {
pub fn new(url: Url) -> Self {
Self {
url,
method: Method::GET,
headers: HeaderMap::new(),
body: None,
cookie_jar: None,
proxy: None,
timeout: None,
max_redirects: 10,
use_header_generator: false,
session_token: None,
}
}
pub fn from_request(request: &Request) -> Self {
Self::new(request.url.clone())
.method(request.method.clone())
.headers(request.headers.clone())
.body_option(request.body.clone())
}
pub fn method(mut self, method: Method) -> Self {
self.method = method;
self
}
pub fn headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
pub fn body(mut self, body: RequestBody) -> Self {
self.body = Some(body);
self
}
fn body_option(mut self, body: Option<RequestBody>) -> Self {
self.body = body;
self
}
pub fn cookie_jar(mut self, cookie_jar: Arc<CookieJar>) -> Self {
self.cookie_jar = Some(cookie_jar);
self
}
pub fn proxy(mut self, proxy: Url) -> Self {
self.proxy = Some(proxy);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn max_redirects(mut self, max_redirects: u32) -> Self {
self.max_redirects = max_redirects;
self
}
pub fn use_header_generator(mut self, enabled: bool) -> Self {
self.use_header_generator = enabled;
self
}
pub fn session_token(mut self, token: crate::session::SessionToken) -> Self {
self.session_token = Some(token);
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct HttpResponse {
pub url: Url,
pub status: StatusCode,
pub headers: HeaderMap,
pub body: Bytes,
pub redirect_chain: Vec<Url>,
}
impl HttpResponse {
pub fn new(url: Url, status: StatusCode, headers: HeaderMap, body: Bytes) -> Self {
Self {
url,
status,
headers,
body,
redirect_chain: Vec::new(),
}
}
pub fn with_redirect_chain(mut self, chain: Vec<Url>) -> Self {
self.redirect_chain = chain;
self
}
pub fn text(&self) -> Cow<'_, str> {
String::from_utf8_lossy(&self.body)
}
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
serde_json::from_slice(&self.body)
}
}
#[non_exhaustive]
pub struct StreamingResponse {
pub url: Url,
pub status: StatusCode,
pub headers: HeaderMap,
pub body: BoxStream<'static, Result<Bytes, HttpClientError>>,
}
impl StreamingResponse {
pub fn new(
url: Url,
status: StatusCode,
headers: HeaderMap,
body: BoxStream<'static, Result<Bytes, HttpClientError>>,
) -> Self {
Self {
url,
status,
headers,
body,
}
}
}
impl fmt::Debug for StreamingResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("StreamingResponse")
.field("url", &self.url)
.field("status", &self.status)
.field("headers", &self.headers)
.finish_non_exhaustive()
}
}
#[async_trait::async_trait]
pub trait HttpClient: Send + Sync + 'static {
async fn send(&self, request: HttpRequest) -> Result<HttpResponse, HttpClientError>;
async fn stream(&self, request: HttpRequest) -> Result<StreamingResponse, HttpClientError>;
}