1use std::collections::HashMap;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::{Arc, Mutex};
5use std::task::{Context, Poll};
6
7use apollo_opentelemetry::metrics::Clock;
8use bytes::Bytes;
9use http_body::Body;
10use http_body_util::BodyExt as _;
11use http_body_util::combinators::UnsyncBoxBody;
12use opentelemetry::{Context as OtelContext, KeyValue};
13use opentelemetry_semantic_conventions::attribute as semconv;
14use tower::util::BoxCloneSyncService;
15use tower::{BoxError, Service};
16
17use crate::builder::HttpClientBuilder;
18use crate::config::HttpClientConfig;
19use crate::error::HttpClientError;
20use crate::metrics::{HttpMetrics, RequestMetrics};
21use crate::protocol::{HostSender, HttpBody, Origin, ProtocolVersionCell};
22use crate::spans::HttpSpans;
23use apollo_http_shared::body::{BodySpanAttr, CountingBody};
24
25pub(crate) struct HttpClientState {
27 pub(crate) pools: HashMap<Origin, HostSender>,
28 pub(crate) make_sender: Box<dyn Fn(Origin) -> HostSender + Send + Sync>,
32}
33
34impl HttpClientState {
35 fn get_or_create(&mut self, origin: Origin) -> HostSender {
37 self.pools
38 .entry(origin)
39 .or_insert_with_key(|key| (self.make_sender)(key.clone()))
40 .clone()
41 }
42}
43
44fn extract_pool_key(uri: &http::Uri) -> Result<Origin, HttpClientError> {
45 match (uri.scheme().cloned(), uri.authority().cloned()) {
46 (Some(scheme), Some(authority)) => Ok(Origin { scheme, authority }),
47 _ => {
48 let err: http::uri::InvalidUri = http::Uri::try_from("").unwrap_err();
52 Err(HttpClientError::InvalidUri { source: err.into() })
53 }
54 }
55}
56
57#[cfg(not(unix))]
60pub(crate) fn unsupported_scheme_sender() -> HostSender {
61 Arc::new(|req, _| {
62 let scheme = req.uri().scheme_str().unwrap_or("").to_owned();
63 Box::pin(async move { Err(HttpClientError::UnsupportedScheme { scheme }) })
64 })
65}
66
67struct ClientGuards {
70 _span_cx: OtelContext,
71 _req_metrics: RequestMetrics,
72}
73
74#[derive(Clone)]
76pub(crate) struct HttpClientDispatch {
77 pub(crate) state: Arc<Mutex<HttpClientState>>,
78 pub(crate) metrics: HttpMetrics,
79 pub(crate) spans: HttpSpans,
80 pub(crate) configured_protocol_version: http::Version,
81}
82
83type HttpClientDispatchFuture = Pin<
84 Box<
85 dyn Future<Output = Result<http::Response<UnsyncBoxBody<Bytes, BoxError>>, HttpClientError>>
86 + Send,
87 >,
88>;
89
90impl Service<http::Request<HttpBody>> for HttpClientDispatch {
91 type Response = http::Response<UnsyncBoxBody<Bytes, BoxError>>;
92 type Error = HttpClientError;
93 type Future = HttpClientDispatchFuture;
94
95 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
96 Poll::Ready(Ok(()))
97 }
98
99 fn call(&mut self, req: http::Request<HttpBody>) -> Self::Future {
100 let state = self.state.clone();
101 let metrics = self.metrics.clone();
102 let spans = self.spans.clone();
103 let protocol_version = ProtocolVersionCell::new(self.configured_protocol_version);
104
105 Box::pin(async move {
106 let span_cx = OtelContext::current();
110 let key = extract_pool_key(req.uri())?;
111 let per_host = state.lock().unwrap().get_or_create(key.clone());
112 let method = req.method().clone();
113
114 let (mut req_metrics, req_body_size) =
115 metrics.begin_request(&method, &key, protocol_version.clone());
116
117 let req_span_attr = spans.request_body_size().then(|| {
118 let attr = BodySpanAttr::new(semconv::HTTP_REQUEST_BODY_SIZE);
119 attr.bind(span_cx.clone());
120 attr
121 });
122 let req = req.map(|body| {
123 CountingBody::new(
124 body,
125 req_body_size.clone(),
126 (),
127 protocol_version.clone(),
128 req_span_attr,
129 )
130 .boxed()
131 });
132
133 match per_host(req, protocol_version.clone()).await {
134 Ok(resp) => {
135 let status = resp.status();
136 req_metrics.on_success(status);
137 req_body_size.set(KeyValue::new(
138 semconv::HTTP_RESPONSE_STATUS_CODE,
139 i64::from(status.as_u16()),
140 ));
141 spans.on_response(protocol_version.get(), &key, status, resp.headers());
142
143 let resp_body_size = metrics.begin_response(&method, &key, status);
144
145 let resp_span_attr = spans.response_body_size().then(|| {
146 let attr = BodySpanAttr::new(semconv::HTTP_RESPONSE_BODY_SIZE);
147 attr.bind(span_cx.clone());
148 attr
149 });
150 let guards = ClientGuards {
151 _span_cx: span_cx,
152 _req_metrics: req_metrics,
153 };
154 let response = resp.map(|body| {
155 CountingBody::new(
156 body,
157 resp_body_size,
158 guards,
159 protocol_version.clone(),
160 resp_span_attr,
161 )
162 .boxed_unsync()
163 });
164 Ok(response)
165 }
166 Err(e) => {
167 req_metrics.on_error();
168 req_body_size.set(KeyValue::new(semconv::ERROR_TYPE, "_OTHER"));
169 spans.on_error(&key, &e.to_string());
170 Err(e)
171 }
172 }
173 })
174 }
175}
176
177pub struct HttpClient {
210 inner: BoxCloneSyncService<
211 http::Request<HttpBody>,
212 http::Response<UnsyncBoxBody<Bytes, BoxError>>,
213 HttpClientError,
214 >,
215}
216
217impl Clone for HttpClient {
218 fn clone(&self) -> Self {
219 Self {
220 inner: self.inner.clone(),
221 }
222 }
223}
224
225impl HttpClient {
226 pub fn new(config: &HttpClientConfig) -> Result<Self, HttpClientError> {
230 HttpClientBuilder::new(config.clone()).build()
231 }
232
233 #[doc(hidden)]
235 pub fn with_clock(config: &HttpClientConfig, clock: Clock) -> Result<Self, HttpClientError> {
236 HttpClientBuilder::new(config.clone()).build_with_clock(clock)
237 }
238
239 pub fn builder(config: HttpClientConfig) -> HttpClientBuilder {
255 HttpClientBuilder::new(config)
256 }
257
258 pub(crate) fn from_service(
259 inner: BoxCloneSyncService<
260 http::Request<HttpBody>,
261 http::Response<UnsyncBoxBody<Bytes, BoxError>>,
262 HttpClientError,
263 >,
264 ) -> Self {
265 Self { inner }
266 }
267}
268
269impl<B> Service<http::Request<B>> for HttpClient
270where
271 B: Body<Data = Bytes> + Send + Sync + 'static,
272 B::Error: Into<BoxError> + Send + Sync + 'static,
273{
274 type Response = http::Response<UnsyncBoxBody<Bytes, BoxError>>;
275 type Error = HttpClientError;
276 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
277
278 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
279 self.inner.poll_ready(cx)
280 }
281
282 fn call(&mut self, req: http::Request<B>) -> Self::Future {
291 let req = req.map(|body| body.map_err(Into::into).boxed());
292 self.inner.call(req)
293 }
294}
295
296const _: fn() = || {
300 fn assert_send_sync<T: Send + Sync>() {}
301 assert_send_sync::<HttpClient>();
302};
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use apollo_opentelemetry_test::{TelemetryContext, assert_metrics_snapshot};
308 use bytes::Bytes;
309 use http_body_util::Full;
310 use tower::ServiceExt as _;
311
312 #[test]
313 fn extract_pool_key_valid_uri() {
314 let uri: http::Uri = "https://example.com/path".parse().unwrap();
315 let Origin { scheme, authority } = extract_pool_key(&uri).unwrap();
316 assert_eq!(scheme, http::uri::Scheme::HTTPS);
317 assert_eq!(authority.host(), "example.com");
318 }
319
320 #[test]
321 fn extract_pool_key_relative_uri_returns_invalid_uri_error() {
322 let uri: http::Uri = "/path/only".parse().unwrap();
323 let err = extract_pool_key(&uri).unwrap_err();
324 assert!(matches!(err, HttpClientError::InvalidUri { .. }));
325 }
326
327 #[test]
328 fn new_with_default_config_succeeds() {
329 let config = HttpClientConfig::default();
330 assert!(HttpClient::new(&config).is_ok());
331 }
332
333 #[test]
334 fn new_returns_invalid_config_when_proxy_url_skips_validation() {
335 use crate::config::{ProxyConfig, ProxyUrl};
339 let url: apollo_configuration::types::Url =
340 "file:///not-a-network-url".parse().expect("parse");
341 let config = HttpClientConfig {
342 proxy: Some(ProxyConfig {
343 url: ProxyUrl::new(url),
344 }),
345 ..HttpClientConfig::default()
346 };
347
348 let err = HttpClient::new(&config)
349 .err()
350 .expect("hostless proxy URL must be rejected");
351 assert!(
352 matches!(err, HttpClientError::InvalidConfig { .. }),
353 "expected InvalidConfig, got {err:?}"
354 );
355 }
356
357 #[tokio::test]
358 async fn records_error_metric_on_failed_request() {
359 let ctx = TelemetryContext::new();
360
361 let config: HttpClientConfig =
362 apollo_configuration::parse_yaml("connect_timeout: 1s", &Default::default())
363 .expect("valid config");
364
365 let (clock, _mock) = Clock::mock();
366 let client = HttpClient::with_clock(&config, clock).expect("valid config");
367 let req = http::Request::builder()
368 .method(http::Method::GET)
369 .uri("http://127.0.0.1:1")
370 .body(
371 Full::new(Bytes::new())
372 .map_err(|never: std::convert::Infallible| match never {})
373 .boxed(),
374 )
375 .unwrap();
376
377 let _ = client.oneshot(req).await;
378
379 assert_metrics_snapshot!(ctx, @r#"
380 - name: http.client.active_requests
381 description: Number of HTTP requests currently in flight
382 unit: "{request}"
383 data:
384 type: Sum
385 data_points:
386 - attributes:
387 http.request.method: GET
388 server.address: 127.0.0.1
389 server.port: "1"
390 value: 0
391 is_monotonic: false
392 temporality: Cumulative
393 - name: http.client.request.duration
394 description: Duration of HTTP client requests
395 unit: s
396 data:
397 type: Histogram
398 data_points:
399 - attributes:
400 error.type: _OTHER
401 http.request.method: GET
402 network.protocol.version: "1.1"
403 server.address: 127.0.0.1
404 server.port: "1"
405 count: 1
406 sum: 0
407 min: 0
408 max: 0
409 bounds:
410 - 0.005
411 - 0.01
412 - 0.025
413 - 0.05
414 - 0.075
415 - 0.1
416 - 0.25
417 - 0.5
418 - 0.75
419 - 1
420 - 2.5
421 - 5
422 - 7.5
423 - 10
424 bucket_counts:
425 - 1
426 - 0
427 - 0
428 - 0
429 - 0
430 - 0
431 - 0
432 - 0
433 - 0
434 - 0
435 - 0
436 - 0
437 - 0
438 - 0
439 - 0
440 temporality: Cumulative
441 "#);
442 }
443}