use http::HeaderMap;
use crate::BuildCustomCaTransportError;
use crate::BuildRouteAwareHttpClientError;
use crate::ClientRouteClass;
use crate::HttpClient;
use crate::HttpClientFactory;
use crate::client::RequestLogging;
use crate::custom_ca::build_reqwest_client_with_custom_ca;
use crate::with_chatgpt_cloudflare_cookie_store;
#[derive(Clone)]
pub struct HttpClientBuilder {
default_headers: Option<HeaderMap>,
follow_redirects: bool,
chatgpt_cloudflare_cookie_store: bool,
request_logging: RequestLogging,
}
impl HttpClientFactory {
pub fn build_client(
&self,
request_url: &str,
route_class: ClientRouteClass,
) -> Result<HttpClient, BuildRouteAwareHttpClientError> {
HttpClientBuilder::new().build_respecting_outbound_proxy_policy(
self,
request_url,
route_class,
)
}
pub fn build_client_without_request_logging(
&self,
request_url: &str,
route_class: ClientRouteClass,
) -> Result<HttpClient, BuildRouteAwareHttpClientError> {
HttpClientBuilder::new()
.without_request_logging()
.build_respecting_outbound_proxy_policy(self, request_url, route_class)
}
}
impl HttpClientBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn default_headers(mut self, headers: HeaderMap) -> Self {
self.default_headers = Some(headers);
self
}
pub fn without_redirects(mut self) -> Self {
self.follow_redirects = false;
self
}
pub fn with_chatgpt_cloudflare_cookie_store(mut self) -> Self {
self.chatgpt_cloudflare_cookie_store = true;
self
}
pub fn without_request_logging(mut self) -> Self {
self.request_logging = RequestLogging::Disabled;
self
}
pub fn build_respecting_outbound_proxy_policy(
self,
http_client_factory: &HttpClientFactory,
request_url: &str,
route_class: ClientRouteClass,
) -> Result<HttpClient, BuildRouteAwareHttpClientError> {
let (builder, request_logging) = self.into_reqwest_parts();
let inner = http_client_factory.build_reqwest_client(builder, request_url, route_class)?;
Ok(HttpClient::from_parts(inner, request_logging))
}
#[deprecated(
note = "legacy compatibility only; use HttpClientFactory::build_client or build_respecting_outbound_proxy_policy"
)]
pub fn build_with_transport_default_proxy(
self,
) -> Result<HttpClient, BuildCustomCaTransportError> {
self.build_with_proxy_routing(ProxyRouting::TransportDefault)
}
pub fn build_direct(self) -> Result<HttpClient, BuildCustomCaTransportError> {
self.build_with_proxy_routing(ProxyRouting::Direct)
}
#[deprecated(
note = "legacy custom-CA fallback only; use HttpClientFactory::build_client or build_respecting_outbound_proxy_policy"
)]
pub fn build_with_transport_default_proxy_and_custom_ca_fallback(self) -> HttpClient {
self.build_with_custom_ca_fallback(ProxyRouting::TransportDefault)
}
#[deprecated(
note = "legacy custom-CA fallback only; use build_direct and propagate construction errors"
)]
pub fn build_direct_with_custom_ca_fallback(self) -> HttpClient {
self.build_with_custom_ca_fallback(ProxyRouting::Direct)
}
fn build_with_proxy_routing(
self,
proxy_routing: ProxyRouting,
) -> Result<HttpClient, BuildCustomCaTransportError> {
let request_logging = self.request_logging;
build_reqwest_client_with_custom_ca(self.reqwest_builder(proxy_routing))
.map(|inner| HttpClient::from_parts(inner, request_logging))
}
fn build_with_custom_ca_fallback(self, proxy_routing: ProxyRouting) -> HttpClient {
self.build_with_custom_ca_fallback_using(proxy_routing, build_reqwest_client_with_custom_ca)
}
fn build_with_custom_ca_fallback_using(
self,
proxy_routing: ProxyRouting,
build_with_custom_ca: impl FnOnce(
reqwest::ClientBuilder,
)
-> Result<reqwest::Client, BuildCustomCaTransportError>,
) -> HttpClient {
let request_logging = self.request_logging;
match build_with_custom_ca(self.clone().reqwest_builder(proxy_routing)) {
Ok(inner) => HttpClient::from_parts(inner, request_logging),
Err(error) => {
tracing::warn!(error = %error, "failed to build HTTP client with custom CA");
self.reqwest_builder(proxy_routing)
.build()
.map(|inner| HttpClient::from_parts(inner, request_logging))
.unwrap_or_else(|fallback_error| {
tracing::warn!(
error = %fallback_error,
"failed to build fallback HTTP client"
);
HttpClient::from_parts(reqwest::Client::new(), request_logging)
})
}
}
}
fn into_reqwest_parts(self) -> (reqwest::ClientBuilder, RequestLogging) {
let request_logging = self.request_logging;
(self.base_reqwest_builder(), request_logging)
}
fn reqwest_builder(self, proxy_routing: ProxyRouting) -> reqwest::ClientBuilder {
let builder = self.base_reqwest_builder();
match proxy_routing {
ProxyRouting::TransportDefault => builder,
ProxyRouting::Direct => builder.no_proxy(),
}
}
fn base_reqwest_builder(self) -> reqwest::ClientBuilder {
let mut builder = reqwest::Client::builder();
if let Some(default_headers) = self.default_headers {
builder = builder.default_headers(default_headers);
}
if !self.follow_redirects {
builder = builder.redirect(reqwest::redirect::Policy::none());
}
if self.chatgpt_cloudflare_cookie_store {
builder = with_chatgpt_cloudflare_cookie_store(builder);
}
builder
}
}
impl Default for HttpClientBuilder {
fn default() -> Self {
Self {
default_headers: None,
follow_redirects: true,
chatgpt_cloudflare_cookie_store: false,
request_logging: RequestLogging::Enabled,
}
}
}
#[derive(Clone, Copy)]
enum ProxyRouting {
TransportDefault,
Direct,
}
#[cfg(test)]
#[path = "client_builder_tests.rs"]
mod tests;