codex_http_client/
client_builder.rs1use http::HeaderMap;
9
10use crate::BuildCustomCaTransportError;
11use crate::BuildRouteAwareHttpClientError;
12use crate::ClientRouteClass;
13use crate::HttpClient;
14use crate::HttpClientFactory;
15use crate::client::RequestLogging;
16use crate::custom_ca::build_reqwest_client_with_custom_ca;
17use crate::with_chatgpt_cloudflare_cookie_store;
18
19#[derive(Clone)]
25pub struct HttpClientBuilder {
26 default_headers: Option<HeaderMap>,
27 follow_redirects: bool,
28 chatgpt_cloudflare_cookie_store: bool,
29 request_logging: RequestLogging,
30}
31
32impl HttpClientFactory {
33 pub fn build_client(
38 &self,
39 request_url: &str,
40 route_class: ClientRouteClass,
41 ) -> Result<HttpClient, BuildRouteAwareHttpClientError> {
42 HttpClientBuilder::new().build_respecting_outbound_proxy_policy(
43 self,
44 request_url,
45 route_class,
46 )
47 }
48
49 pub fn build_client_without_request_logging(
53 &self,
54 request_url: &str,
55 route_class: ClientRouteClass,
56 ) -> Result<HttpClient, BuildRouteAwareHttpClientError> {
57 HttpClientBuilder::new()
58 .without_request_logging()
59 .build_respecting_outbound_proxy_policy(self, request_url, route_class)
60 }
61}
62
63impl HttpClientBuilder {
64 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn default_headers(mut self, headers: HeaderMap) -> Self {
69 self.default_headers = Some(headers);
70 self
71 }
72
73 pub fn without_redirects(mut self) -> Self {
74 self.follow_redirects = false;
75 self
76 }
77
78 pub fn with_chatgpt_cloudflare_cookie_store(mut self) -> Self {
79 self.chatgpt_cloudflare_cookie_store = true;
80 self
81 }
82
83 pub fn without_request_logging(mut self) -> Self {
85 self.request_logging = RequestLogging::Disabled;
86 self
87 }
88
89 pub fn build_respecting_outbound_proxy_policy(
95 self,
96 http_client_factory: &HttpClientFactory,
97 request_url: &str,
98 route_class: ClientRouteClass,
99 ) -> Result<HttpClient, BuildRouteAwareHttpClientError> {
100 let (builder, request_logging) = self.into_reqwest_parts();
101 let inner = http_client_factory.build_reqwest_client(builder, request_url, route_class)?;
102 Ok(HttpClient::from_parts(inner, request_logging))
103 }
104
105 #[deprecated(
113 note = "legacy compatibility only; use HttpClientFactory::build_client or build_respecting_outbound_proxy_policy"
114 )]
115 pub fn build_with_transport_default_proxy(
116 self,
117 ) -> Result<HttpClient, BuildCustomCaTransportError> {
118 self.build_with_proxy_routing(ProxyRouting::TransportDefault)
119 }
120
121 pub fn build_direct(self) -> Result<HttpClient, BuildCustomCaTransportError> {
131 self.build_with_proxy_routing(ProxyRouting::Direct)
132 }
133
134 #[deprecated(
142 note = "legacy custom-CA fallback only; use HttpClientFactory::build_client or build_respecting_outbound_proxy_policy"
143 )]
144 pub fn build_with_transport_default_proxy_and_custom_ca_fallback(self) -> HttpClient {
145 self.build_with_custom_ca_fallback(ProxyRouting::TransportDefault)
146 }
147
148 #[deprecated(
155 note = "legacy custom-CA fallback only; use build_direct and propagate construction errors"
156 )]
157 pub fn build_direct_with_custom_ca_fallback(self) -> HttpClient {
158 self.build_with_custom_ca_fallback(ProxyRouting::Direct)
159 }
160
161 fn build_with_proxy_routing(
162 self,
163 proxy_routing: ProxyRouting,
164 ) -> Result<HttpClient, BuildCustomCaTransportError> {
165 let request_logging = self.request_logging;
166 build_reqwest_client_with_custom_ca(self.reqwest_builder(proxy_routing))
167 .map(|inner| HttpClient::from_parts(inner, request_logging))
168 }
169
170 fn build_with_custom_ca_fallback(self, proxy_routing: ProxyRouting) -> HttpClient {
171 self.build_with_custom_ca_fallback_using(proxy_routing, build_reqwest_client_with_custom_ca)
172 }
173
174 fn build_with_custom_ca_fallback_using(
175 self,
176 proxy_routing: ProxyRouting,
177 build_with_custom_ca: impl FnOnce(
178 reqwest::ClientBuilder,
179 )
180 -> Result<reqwest::Client, BuildCustomCaTransportError>,
181 ) -> HttpClient {
182 let request_logging = self.request_logging;
183 match build_with_custom_ca(self.clone().reqwest_builder(proxy_routing)) {
184 Ok(inner) => HttpClient::from_parts(inner, request_logging),
185 Err(error) => {
186 tracing::warn!(error = %error, "failed to build HTTP client with custom CA");
187 self.reqwest_builder(proxy_routing)
188 .build()
189 .map(|inner| HttpClient::from_parts(inner, request_logging))
190 .unwrap_or_else(|fallback_error| {
191 tracing::warn!(
192 error = %fallback_error,
193 "failed to build fallback HTTP client"
194 );
195 HttpClient::from_parts(reqwest::Client::new(), request_logging)
196 })
197 }
198 }
199 }
200
201 fn into_reqwest_parts(self) -> (reqwest::ClientBuilder, RequestLogging) {
202 let request_logging = self.request_logging;
203 (self.base_reqwest_builder(), request_logging)
204 }
205
206 fn reqwest_builder(self, proxy_routing: ProxyRouting) -> reqwest::ClientBuilder {
207 let builder = self.base_reqwest_builder();
208 match proxy_routing {
209 ProxyRouting::TransportDefault => builder,
210 ProxyRouting::Direct => builder.no_proxy(),
211 }
212 }
213
214 fn base_reqwest_builder(self) -> reqwest::ClientBuilder {
215 let mut builder = reqwest::Client::builder();
216 if let Some(default_headers) = self.default_headers {
217 builder = builder.default_headers(default_headers);
218 }
219 if !self.follow_redirects {
220 builder = builder.redirect(reqwest::redirect::Policy::none());
221 }
222 if self.chatgpt_cloudflare_cookie_store {
223 builder = with_chatgpt_cloudflare_cookie_store(builder);
224 }
225 builder
226 }
227}
228
229impl Default for HttpClientBuilder {
230 fn default() -> Self {
231 Self {
232 default_headers: None,
233 follow_redirects: true,
234 chatgpt_cloudflare_cookie_store: false,
235 request_logging: RequestLogging::Enabled,
236 }
237 }
238}
239
240#[derive(Clone, Copy)]
241enum ProxyRouting {
242 TransportDefault,
243 Direct,
244}
245
246#[cfg(test)]
247#[path = "client_builder_tests.rs"]
248mod tests;