apollo_http_client/config.rs
1use apollo_configuration::ErrorCollector;
2use apollo_configuration::Validate;
3use apollo_configuration::configuration;
4use apollo_configuration::types::{Duration, HeaderName, Url};
5use apollo_redaction::Redacted;
6use apollo_redaction::strategy::UrlPasswordRedactor;
7use std::num::NonZeroUsize;
8use std::time::Duration as StdDuration;
9
10/// A proxy URL with the password component redacted in `Debug` and `Display` output.
11pub type ProxyUrl = Redacted<Url, UrlPasswordRedactor>;
12
13/// HTTP protocol version to use for connections.
14#[non_exhaustive]
15#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize, schemars::JsonSchema)]
16#[serde(rename_all = "snake_case")]
17pub enum Protocol {
18 /// HTTP/1.1 only (default).
19 #[default]
20 Http1,
21 /// HTTP/2 only.
22 ///
23 /// Over HTTPS, ALPN is configured automatically — the server selects HTTP/2 during
24 /// the TLS handshake if it supports it. Over plain HTTP and `unix://`, prior-knowledge
25 /// HTTP/2 is used: the client speaks HTTP/2 directly without an Upgrade round trip.
26 /// Use this only when you know the server supports HTTP/2 on that transport.
27 Http2,
28 /// Negotiate HTTP/1.1 or HTTP/2 automatically.
29 ///
30 /// Over HTTPS, ALPN is used: the server selects HTTP/1.1 or HTTP/2 during the TLS
31 /// handshake. Over plain HTTP and `unix://`, there is no TLS handshake, so ALPN
32 /// cannot negotiate a protocol and the connection always falls back to HTTP/1.1 —
33 /// use `http2` explicitly for prior-knowledge HTTP/2 on those transports.
34 Alpn,
35}
36
37impl Validate for Protocol {}
38
39/// HTTP/2 connection settings.
40///
41/// These apply when `protocol` is `http2`, and to HTTP/2 connections
42/// negotiated under `protocol: alpn`.
43#[non_exhaustive]
44#[configuration]
45pub struct Http2Config {
46 /// How long an idle HTTP/2 connection is kept before being closed. The
47 /// connection is considered idle when no requests are in flight.
48 #[config(default = Duration::from(StdDuration::from_secs(300)))]
49 pub connection_idle_timeout: Duration,
50
51 /// Interval at which HTTP/2 PING frames are sent to detect dead
52 /// connections and prevent proxies from closing idle connections.
53 #[config(default = Duration::from(StdDuration::from_secs(30)))]
54 pub keep_alive_interval: Duration,
55
56 /// How long to wait for a PING acknowledgement before closing the
57 /// connection.
58 #[config(default = Duration::from(StdDuration::from_secs(20)))]
59 pub keep_alive_timeout: Duration,
60
61 /// Whether to send keep-alive pings when there are no active streams.
62 #[config(default = true)]
63 pub keep_alive_while_idle: bool,
64}
65
66/// TCP socket settings applied to every outbound connection.
67///
68/// These apply regardless of HTTP protocol version (HTTP/1.1, HTTP/2, or
69/// negotiated under `protocol: alpn`).
70#[non_exhaustive]
71#[configuration]
72pub struct TcpConfig {
73 /// Whether to disable Nagle's algorithm (`TCP_NODELAY`).
74 ///
75 /// When `true` (default), small payloads are sent immediately, reducing
76 /// tail latency on small request/response bodies. Set to `false` to enable
77 /// Nagle's algorithm, in which the kernel may briefly buffer small writes
78 /// to coalesce them into fewer segments.
79 #[config(default = true)]
80 pub nodelay: bool,
81
82 /// Idle period before the OS starts sending TCP keep-alive probes.
83 ///
84 /// Defaults to 60 s. Probes detect dead connections before a new request
85 /// is dispatched on them and prevent intermediate proxies (load balancers,
86 /// NATs) from silently dropping long-lived idle connections.
87 #[config(default = Duration::from(StdDuration::from_secs(60)))]
88 pub keepalive: Duration,
89}
90
91/// HTTP/1.1 connection pool settings.
92///
93/// These apply when `protocol` is `http1`, and to HTTP/1.1 connections
94/// negotiated under `protocol: alpn`. They have no effect when `protocol`
95/// is `http2` — HTTP/2 uses a single persistent connection per host.
96#[non_exhaustive]
97#[configuration]
98pub struct Http1Config {
99 /// Maximum idle connections kept open per host.
100 #[config(default = NonZeroUsize::new(10).unwrap())]
101 pub pool_max_idle_per_host: NonZeroUsize,
102
103 /// How long idle connections are kept alive before being closed.
104 #[config(default = Duration::from(StdDuration::from_secs(90)))]
105 pub pool_idle_timeout: Duration,
106}
107
108/// Metrics opt-in settings.
109///
110/// Controls which optional OTel HTTP client metrics are recorded and which opt-in attributes
111/// are included. All are disabled by default, as required by the OTel HTTP semantic conventions.
112#[non_exhaustive]
113#[configuration]
114pub struct MetricsConfig {
115 /// Enable recording of `http.client.request.body.size`.
116 ///
117 /// Opt-In per the OTel HTTP semantic conventions.
118 pub request_body_size: bool,
119
120 /// Enable recording of `http.client.response.body.size`.
121 ///
122 /// Opt-In per the OTel HTTP semantic conventions.
123 pub response_body_size: bool,
124
125 /// Include the `url.scheme` attribute on all HTTP client metrics.
126 ///
127 /// Opt-In per the OTel HTTP semantic conventions.
128 pub url_scheme: bool,
129}
130
131/// Spans opt-in settings.
132#[non_exhaustive]
133#[configuration]
134pub struct SpansConfig {
135 /// Capture `url.scheme` as a span attribute.
136 ///
137 /// Opt-In per the OTel HTTP semantic conventions.
138 pub url_scheme: bool,
139
140 /// Capture `http.request.body.size` as a span attribute (actual bytes sent).
141 ///
142 /// Opt-In per the OTel HTTP semantic conventions.
143 pub request_body_size: bool,
144
145 /// Capture `http.response.body.size` as a span attribute (actual bytes received).
146 ///
147 /// Opt-In per the OTel HTTP semantic conventions.
148 pub response_body_size: bool,
149
150 /// Capture `user_agent.original` from the outgoing `User-Agent` request header.
151 ///
152 /// Opt-In per the OTel HTTP semantic conventions.
153 pub user_agent: bool,
154
155 /// Capture `network.transport` as a span attribute. Always `"tcp"` for HTTP/1.1 and HTTP/2.
156 ///
157 /// Opt-In per the OTel HTTP semantic conventions.
158 pub network_transport: bool,
159
160 /// Static `network.local.address` span attribute: the outbound IP address of this service
161 /// (e.g. `"192.168.1.10"`). Opt-In per the OTel HTTP semantic conventions.
162 pub network_local_address: Option<String>,
163
164 /// Static `network.local.port` span attribute: the source port this service binds to, if fixed.
165 /// Opt-In per the OTel HTTP semantic conventions.
166 pub network_local_port: Option<u16>,
167
168 /// HTTP request header names to capture as span attributes (case-insensitive).
169 ///
170 /// Each header `x-foo` becomes `http.request.header.x-foo` with a string-array value.
171 /// Opt-In per the OTel HTTP semantic conventions.
172 #[config(skip_validate)]
173 pub request_headers: Vec<HeaderName>,
174
175 /// HTTP response header names to capture as span attributes (case-insensitive).
176 ///
177 /// Each header `x-foo` becomes `http.response.header.x-foo` with a string-array value.
178 /// Opt-In per the OTel HTTP semantic conventions.
179 #[config(skip_validate)]
180 pub response_headers: Vec<HeaderName>,
181}
182
183/// Telemetry settings.
184#[non_exhaustive]
185#[configuration]
186pub struct TelemetryConfig {
187 /// Metrics opt-in settings.
188 pub metrics: MetricsConfig,
189
190 /// Spans opt-in settings.
191 pub spans: SpansConfig,
192}
193
194/// Proxy server configuration.
195///
196/// Routes outbound `http://` and `https://` requests through a proxy server.
197/// `unix://` requests bypass the proxy and reach the socket directly.
198///
199/// ```yaml
200/// proxy:
201/// url: "http://user:${PROXY_PASSWORD}@proxy.corp.example.com:3128"
202/// ```
203#[non_exhaustive]
204#[configuration]
205pub struct ProxyConfig {
206 /// Proxy server URL.
207 ///
208 /// Supports Basic authentication via embedded credentials:
209 /// `http://user:password@proxy.corp.example.com:3128`
210 ///
211 /// Use env-var expansion to avoid hardcoding addresses or credentials:
212 /// `url: "${EGRESS_PROXY_URL}"`
213 #[config(required, validate = validate_proxy_url)]
214 pub url: ProxyUrl,
215}
216
217fn validate_proxy_url(url: &ProxyUrl, mut errors: ErrorCollector<'_>) {
218 let url = url.unredact();
219 if url.host_str().is_none_or(str::is_empty) {
220 errors.report_simple("proxy URL must include a host");
221 }
222 match url.scheme() {
223 "http" | "https" => {}
224 other => errors.report_simple(format!(
225 "unsupported proxy URL scheme `{other}`; must be http or https"
226 )),
227 }
228}
229
230/// TLS settings for outbound connections.
231#[non_exhaustive]
232#[configuration]
233pub struct TlsConfig {
234 /// PEM-encoded CA certificate bundle to trust. Multiple certificates may
235 /// be concatenated in a single PEM blob. When `use_native_certificate_store`
236 /// is also enabled (the default), these CAs are added to the OS native
237 /// store; set `use_native_certificate_store: false` to trust only the
238 /// supplied bundle.
239 ///
240 /// Typically loaded from disk with `${file:./certs/ca.pem}`.
241 pub certificate_authorities: Option<Redacted<String>>,
242
243 /// Whether the OS native certificate store is trusted. When `true` (the
244 /// default), any CAs supplied via `certificate_authorities` are layered
245 /// on top. Set to `false` to trust only the supplied bundle — useful in
246 /// air-gapped environments.
247 #[config(default = true)]
248 pub use_native_certificate_store: bool,
249
250 /// Client certificate and key to present during mutual TLS handshakes.
251 /// Omit to disable client authentication.
252 pub client_authentication: Option<ClientAuthentication>,
253
254 /// Skip TLS certificate verification. **Never use in production.** When
255 /// enabled, the client accepts any certificate regardless of issuer or
256 /// validity, making all outbound HTTPS connections vulnerable to
257 /// machine-in-the-middle interception.
258 pub danger_accept_invalid_certs: bool,
259}
260
261/// Client identity used for mutual TLS authentication.
262///
263/// The certificate is offered to servers during the TLS handshake; servers
264/// that do not require client authentication ignore it. Encrypted private
265/// keys are not supported.
266#[non_exhaustive]
267#[configuration]
268pub struct ClientAuthentication {
269 /// PEM-encoded client certificate followed by any intermediate
270 /// certificates needed to complete the chain to a CA the server trusts.
271 ///
272 /// Typically loaded from disk with `${file:./certs/client.pem}`.
273 #[config(required)]
274 pub certificate_chain: Redacted<String>,
275
276 /// PEM-encoded unencrypted private key matching the leaf certificate in
277 /// `certificate_chain`. PKCS#1, PKCS#8, and SEC1 formats are accepted.
278 ///
279 /// Typically loaded from disk with `${file:./certs/client.key}`.
280 #[config(required)]
281 pub key: Redacted<String>,
282}
283
284/// HTTP client configuration.
285///
286/// Controls the protocol, connection pool, timeouts, and opt-in OTel telemetry.
287#[non_exhaustive]
288#[configuration]
289pub struct HttpClientConfig {
290 /// Connection timeout covering both the TCP dial and TLS handshake.
291 #[config(default = Duration::from(StdDuration::from_secs(10)))]
292 pub connect_timeout: Duration,
293
294 /// HTTP/1.1 connection pool settings.
295 pub http1: Http1Config,
296
297 /// HTTP/2 connection settings.
298 pub http2: Http2Config,
299
300 /// HTTP protocol version to use.
301 pub protocol: Protocol,
302
303 /// TCP socket settings applied to every outbound connection.
304 pub tcp: TcpConfig,
305
306 /// Telemetry opt-in settings.
307 pub telemetry: TelemetryConfig,
308
309 /// TLS settings for outbound connections.
310 pub tls: TlsConfig,
311
312 /// Proxy server configuration. When set, outbound `http://` and `https://`
313 /// requests are routed through the specified proxy; `unix://` requests
314 /// always reach the socket directly.
315 ///
316 /// When absent (the default), TCP connections are made directly to the target.
317 pub proxy: Option<ProxyConfig>,
318}