Skip to main content

apollo_http_client/
builder.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use apollo_opentelemetry::metrics::Clock;
5use apollo_opentelemetry::tower::ServiceBuilderExt as _;
6use hyper_util::client::legacy::connect::HttpConnector;
7use rustls::ClientConfig;
8use tower::ServiceBuilder;
9use tower::util::BoxCloneSyncService;
10
11use crate::client::{HttpClient, HttpClientDispatch, HttpClientState};
12use crate::config::{HttpClientConfig, Protocol};
13use crate::error::HttpClientError;
14use crate::metrics::HttpMetrics;
15use crate::protocol::h1::H1Connector;
16use crate::protocol::h2::H2Connector;
17use crate::protocol::{HostSender, Origin};
18use crate::spans::HttpSpans;
19use crate::tls::{build_https_connector, build_tls_config, with_protocol_alpn};
20
21/// Builder for [`HttpClient`].
22///
23/// Obtain via [`HttpClient::builder`].
24pub struct HttpClientBuilder {
25    config: HttpClientConfig,
26    tls_override: Option<Arc<ClientConfig>>,
27}
28
29impl HttpClientBuilder {
30    pub(crate) fn new(config: HttpClientConfig) -> Self {
31        Self {
32            config,
33            tls_override: None,
34        }
35    }
36
37    /// Injects a pre-built TLS configuration, bypassing the YAML-driven TLS
38    /// settings in [`HttpClientConfig`].
39    ///
40    /// The `protocol` field on the supplied config still governs ALPN
41    /// negotiation — any ALPN protocols on the injected `ClientConfig` are
42    /// overwritten to match.
43    pub fn with_tls_config(mut self, tls: Arc<ClientConfig>) -> Self {
44        self.tls_override = Some(tls);
45        self
46    }
47
48    /// Builds the [`HttpClient`].
49    ///
50    /// Returns an error if the TLS connector cannot be constructed.
51    pub fn build(self) -> Result<HttpClient, HttpClientError> {
52        self.build_with_clock(Clock::new())
53    }
54
55    pub(crate) fn build_with_clock(self, clock: Clock) -> Result<HttpClient, HttpClientError> {
56        let HttpClientBuilder {
57            config,
58            tls_override,
59        } = self;
60
61        // Re-run validation defensively so callers that bypass
62        // `apollo_configuration::parse_yaml` still get the same parse-time
63        // guarantees (well-formed proxy URL, supported scheme, …). `parse_yaml`
64        // already calls this; the cost is negligible and the result is a single
65        // source of truth for what a valid `HttpClientConfig` looks like.
66        apollo_configuration::validate(config.clone())
67            .map_err(|source| HttpClientError::InvalidConfig { source })?;
68
69        let mut http = HttpConnector::new();
70        http.enforce_http(false);
71        http.set_nodelay(config.tcp.nodelay);
72        http.set_keepalive(Some(*config.tcp.keepalive));
73
74        let tls_config: Arc<ClientConfig> = match tls_override {
75            Some(ref base) => Arc::new(with_protocol_alpn(base, config.protocol)),
76            None => build_tls_config(config.protocol, &config.tls)?,
77        };
78
79        let connect_timeout = *config.connect_timeout;
80
81        let configured_protocol_version = match config.protocol {
82            Protocol::Http2 => http::Version::HTTP_2,
83            Protocol::Http1 | Protocol::Alpn => http::Version::HTTP_11,
84        };
85        let metrics = HttpMetrics::new(
86            clock,
87            config.telemetry.metrics.request_body_size,
88            config.telemetry.metrics.response_body_size,
89            config.telemetry.metrics.url_scheme,
90        );
91        let conn_metrics = metrics.connection_metrics();
92        let h1_max_idle = config.http1.pool_max_idle_per_host.get();
93        let h1_idle_timeout = *config.http1.pool_idle_timeout;
94        let h2_idle_timeout = *config.http2.connection_idle_timeout;
95
96        #[cfg(unix)]
97        let unix_config = Arc::new(crate::protocol::unix::UnixSenderConfig {
98            protocol: config.protocol,
99            connect_timeout,
100            h1_max_idle,
101            h1_idle_timeout,
102            h2_idle_timeout,
103            keep_alive_interval: *config.http2.keep_alive_interval,
104            keep_alive_timeout: *config.http2.keep_alive_timeout,
105            keep_alive_while_idle: config.http2.keep_alive_while_idle,
106            conn_metrics: conn_metrics.clone(),
107        });
108
109        let tcp_make_sender: Box<dyn Fn(Origin) -> HostSender + Send + Sync> =
110            if let Some(proxy) = &config.proxy {
111                let proxy_url = proxy.url.unredact();
112                let proxy_host = proxy_url
113                    .host_str()
114                    .expect("validate above rejects URLs with no host")
115                    .to_string();
116                let proxy_port = proxy_url.port_or_known_default().expect(
117                    "validate above rejects non-http(s) schemes, both of which have default ports",
118                );
119                // Tag connection metrics with the proxy as `network.peer.*` so
120                // operators can tell from telemetry that traffic was proxied.
121                let conn_metrics = conn_metrics.with_peer(proxy_host.as_str(), proxy_port);
122                let proxy_config = Arc::new(crate::proxy::ProxySenderConfig {
123                    proxy_host,
124                    proxy_port,
125                    proxy_auth: crate::proxy::proxy_authorization(proxy_url),
126                    tls_config,
127                    protocol: config.protocol,
128                    connect_timeout,
129                    h1_max_idle,
130                    h1_idle_timeout,
131                    h2_idle_timeout,
132                    keep_alive_interval: *config.http2.keep_alive_interval,
133                    keep_alive_timeout: *config.http2.keep_alive_timeout,
134                    keep_alive_while_idle: config.http2.keep_alive_while_idle,
135                    conn_metrics,
136                });
137                Box::new(move |key| crate::proxy::new_sender(&proxy_config, key))
138            } else {
139                let https = build_https_connector(http, tls_config, config.protocol);
140                let h1_connector =
141                    H1Connector::new(https.clone(), conn_metrics.clone(), connect_timeout);
142                let h2_connector = H2Connector::new(
143                    https.clone(),
144                    conn_metrics.clone(),
145                    connect_timeout,
146                    *config.http2.keep_alive_interval,
147                    *config.http2.keep_alive_timeout,
148                    config.http2.keep_alive_while_idle,
149                );
150                match config.protocol {
151                    Protocol::Http2 => Box::new(move |key| {
152                        crate::protocol::h2::new_sender(
153                            h2_connector.clone(),
154                            h2_idle_timeout,
155                            key,
156                            |_| {},
157                        )
158                    }),
159                    Protocol::Http1 => Box::new(move |key| {
160                        crate::protocol::h1::new_sender(
161                            h1_connector.clone(),
162                            h1_max_idle,
163                            h1_idle_timeout,
164                            key,
165                            |_req| {},
166                        )
167                    }),
168                    Protocol::Alpn => Box::new(move |key| {
169                        crate::protocol::alpn::new_sender(
170                            h1_connector.clone(),
171                            h1_max_idle,
172                            h1_idle_timeout,
173                            h2_connector.clone(),
174                            h2_idle_timeout,
175                            key,
176                        )
177                    }),
178                }
179            };
180
181        // Route `unix://` keys through the Unix socket connector; everything else
182        // goes through the TCP/TLS path built above. Proxy configuration applies
183        // only to the TCP path.
184        let make_sender: Box<dyn Fn(Origin) -> HostSender + Send + Sync> = Box::new(move |key| {
185            if key.scheme.as_str() == "unix" {
186                #[cfg(unix)]
187                {
188                    crate::protocol::unix::new_sender(&unix_config, key)
189                }
190                #[cfg(not(unix))]
191                {
192                    crate::client::unsupported_scheme_sender()
193                }
194            } else {
195                tcp_make_sender(key)
196            }
197        });
198
199        let spans = HttpSpans::new(config.telemetry.spans.clone());
200        let dispatch = HttpClientDispatch {
201            state: Arc::new(Mutex::new(HttpClientState {
202                pools: HashMap::new(),
203                make_sender,
204            })),
205            metrics,
206            spans: spans.clone(),
207            configured_protocol_version,
208        };
209        let service = ServiceBuilder::new()
210            .traced(
211                apollo_opentelemetry::default_instrumentation_scope!(),
212                spans,
213            )
214            .http_client_propagation()
215            .service(dispatch);
216        let inner = BoxCloneSyncService::new(service);
217
218        Ok(HttpClient::from_service(inner))
219    }
220}