saluki_io/net/client/http/
client.rs1#[cfg(unix)]
2use std::path::PathBuf;
3use std::{
4 future::Future,
5 pin::Pin,
6 task::{Context, Poll},
7 time::Duration,
8};
9
10use bytes::{Buf, Bytes};
11use http::{Request, Response, Uri};
12use http_body::{Body, Frame, SizeHint};
13use http_body_util::combinators::BoxBody;
14use hyper::body::Incoming;
15use hyper_http_proxy::Proxy;
16use hyper_util::{
17 client::legacy::{connect::capture_connection, Builder},
18 rt::{TokioExecutor, TokioTimer},
19};
20use metrics::Counter;
21use pin_project::pin_project;
22use saluki_error::GenericError;
23use saluki_metrics::MetricsBuilder;
24use saluki_tls::{ClientTLSConfigBuilder, TlsMinimumVersion};
25use stringtheory::MetaString;
26use tower::{timeout::TimeoutLayer, util::BoxCloneService, BoxError, Service, ServiceBuilder, ServiceExt as _};
27
28use super::{
29 conn::{check_connection_state, HttpProtocol, HttpsCapableConnectorBuilder},
30 telemetry::HttpTransactionErrorTelemetry,
31 EndpointTelemetryLayer,
32};
33
34pub type ClientBody = BoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>>;
39
40#[pin_project]
41struct ClientBodyAdapter<B> {
42 #[pin]
43 inner: B,
44 size_hint: SizeHint,
45}
46
47impl<B> Body for ClientBodyAdapter<B>
48where
49 B: Body,
50 B::Data: Buf,
51 B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
52{
53 type Data = Bytes;
54 type Error = Box<dyn std::error::Error + Send + Sync>;
55
56 fn poll_frame(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
57 self.project().inner.poll_frame(cx).map(|maybe_frame| {
58 maybe_frame.map(|result| {
59 result
60 .map(|frame| frame.map_data(|mut data| data.copy_to_bytes(data.remaining())))
61 .map_err(Into::into)
62 })
63 })
64 }
65
66 fn size_hint(&self) -> SizeHint {
67 self.size_hint.clone()
68 }
69}
70
71#[derive(Clone)]
73pub struct HttpClient {
74 inner: BoxCloneService<Request<ClientBody>, Response<Incoming>, BoxError>,
75}
76
77impl HttpClient {
78 pub fn builder() -> HttpClientBuilder {
80 HttpClientBuilder::default()
81 }
82
83 pub async fn send<B>(&mut self, req: Request<B>) -> Result<Response<Incoming>, GenericError>
92 where
93 B: Body + Send + Sync + 'static,
94 B::Data: Buf + Send,
95 B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
96 {
97 let mut req = req.map(into_client_body);
98 let captured_conn = capture_connection(&mut req);
99 let result = self
100 .inner
101 .ready()
102 .await
103 .map_err(GenericError::from_boxed)?
104 .call(req)
105 .await;
106
107 check_connection_state(captured_conn);
108
109 result.map_err(GenericError::from_boxed)
110 }
111}
112
113impl Service<Request<ClientBody>> for HttpClient {
114 type Response = Response<Incoming>;
115 type Error = BoxError;
116 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
117
118 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
119 self.inner.poll_ready(cx)
120 }
121
122 fn call(&mut self, mut req: Request<ClientBody>) -> Self::Future {
123 let captured_conn = capture_connection(&mut req);
124 let fut = self.inner.call(req);
125
126 Box::pin(async move {
127 let result = fut.await;
128
129 check_connection_state(captured_conn);
130
131 result
132 })
133 }
134}
135
136pub fn into_client_body<B>(body: B) -> ClientBody
141where
142 B: Body + Send + Sync + 'static,
143 B::Data: Buf + Send,
144 B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
145{
146 let size_hint = body.size_hint();
147 BoxBody::new(ClientBodyAdapter { inner: body, size_hint })
148}
149
150pub struct HttpClientBuilder {
165 connector_builder: HttpsCapableConnectorBuilder,
166 hyper_builder: Builder,
167 tls_builder: ClientTLSConfigBuilder,
168 request_timeout: Option<Duration>,
169 endpoint_telemetry: Option<EndpointTelemetryLayer>,
170 proxies: Option<Vec<Proxy>>,
171}
172
173impl HttpClientBuilder {
174 pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
178 self.connector_builder = self.connector_builder.with_connect_timeout(timeout);
179 self
180 }
181
182 pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
189 self.request_timeout = Some(timeout);
190 self
191 }
192
193 pub fn without_request_timeout(mut self) -> Self {
198 self.request_timeout = None;
199 self
200 }
201
202 pub fn with_http_protocol(mut self, protocol: HttpProtocol) -> Self {
206 self.connector_builder = self.connector_builder.with_http_protocol(protocol);
207 self
208 }
209
210 pub fn with_connection_age_limit<L>(mut self, limit: L) -> Self
217 where
218 L: Into<Option<Duration>>,
219 {
220 self.connector_builder = self.connector_builder.with_connection_age_limit(limit);
221 self
222 }
223
224 pub fn with_max_idle_conns_per_host(mut self, max: usize) -> Self {
228 self.hyper_builder.pool_max_idle_per_host(max);
229 self
230 }
231
232 pub fn with_idle_conn_timeout(mut self, timeout: Duration) -> Self {
239 self.hyper_builder.pool_idle_timeout(timeout);
240 self
241 }
242
243 pub fn with_proxies(mut self, proxies: Vec<Proxy>) -> Self {
247 self.proxies = Some(proxies);
248 self
249 }
250
251 pub fn with_endpoint_telemetry<F>(mut self, metrics_builder: MetricsBuilder, endpoint_name_fn: Option<F>) -> Self
255 where
256 F: Fn(&Uri) -> Option<MetaString> + Send + Sync + 'static,
257 {
258 let error_telemetry = HttpTransactionErrorTelemetry::from_builder(&metrics_builder);
259 self.connector_builder = self.connector_builder.with_error_telemetry(error_telemetry.clone());
260
261 let mut layer = EndpointTelemetryLayer::default()
262 .with_metrics_builder(metrics_builder)
263 .with_error_telemetry(error_telemetry);
264
265 if let Some(endpoint_name_fn) = endpoint_name_fn {
266 layer = layer.with_endpoint_name_fn(endpoint_name_fn);
267 }
268
269 self.endpoint_telemetry = Some(layer);
270 self
271 }
272
273 #[cfg(unix)]
281 pub fn with_unix_socket_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
282 self.connector_builder = self.connector_builder.with_unix_socket_path(path);
283 self
284 }
285
286 pub fn with_tls_config<F>(mut self, f: F) -> Self
290 where
291 F: FnOnce(ClientTLSConfigBuilder) -> ClientTLSConfigBuilder,
292 {
293 self.tls_builder = f(self.tls_builder);
294 self
295 }
296
297 pub fn with_min_tls_version(mut self, version: TlsMinimumVersion) -> Self {
304 self.tls_builder = self.tls_builder.with_min_tls_version(version);
305 self
306 }
307
308 pub fn with_hyper_config<F>(mut self, f: F) -> Self
313 where
314 F: FnOnce(&mut Builder),
315 {
316 f(&mut self.hyper_builder);
317 self
318 }
319
320 pub fn with_bytes_sent_counter(mut self, counter: Counter) -> Self {
327 self.connector_builder = self.connector_builder.with_bytes_sent_counter(counter);
328 self
329 }
330
331 pub fn build(self) -> Result<HttpClient, GenericError> {
337 let tls_config = self.tls_builder.build()?;
338 let connector = self.connector_builder.build(tls_config)?;
339 let mut proxy_connector = hyper_http_proxy::ProxyConnector::new(connector)?;
343 if let Some(proxies) = &self.proxies {
344 for proxy in proxies {
345 proxy_connector.add_proxy(proxy.to_owned());
346 }
347 }
348 let client = self.hyper_builder.build(proxy_connector);
349
350 let inner = ServiceBuilder::new()
351 .option_layer(self.request_timeout.map(TimeoutLayer::new))
352 .option_layer(self.endpoint_telemetry)
353 .service(client.map_err(BoxError::from))
354 .boxed_clone();
355
356 Ok(HttpClient { inner })
357 }
358}
359
360impl Default for HttpClientBuilder {
361 fn default() -> Self {
362 let mut hyper_builder = Builder::new(TokioExecutor::new());
363 hyper_builder
364 .pool_timer(TokioTimer::new())
365 .pool_max_idle_per_host(5)
366 .pool_idle_timeout(Duration::from_secs(45));
367
368 Self {
369 connector_builder: HttpsCapableConnectorBuilder::default(),
370 hyper_builder,
371 tls_builder: ClientTLSConfigBuilder::new(),
372 request_timeout: Some(Duration::from_secs(20)),
373 endpoint_telemetry: None,
374 proxies: None,
375 }
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use http_body::Body as _;
382 use http_body_util::Full;
383
384 use super::*;
385
386 #[test]
387 fn into_client_body_preserves_exact_size_hint() {
388 let body = Full::new(Bytes::from_static(b"hello"));
389 let converted = into_client_body(body);
390
391 assert_eq!(Some(5), converted.size_hint().exact());
392 }
393}