apollo_http_client/protocol/
mod.rs1use std::future::Future;
2use std::pin::Pin;
3use std::sync::{Arc, Mutex};
4use std::time::Duration;
5use tokio::time::Instant;
6use tokio_util::sync::CancellationToken;
7
8use bytes::Bytes;
9use http_body_util::combinators::BoxBody;
10use hyper::body::Incoming;
11use tower::BoxError;
12
13pub(crate) use apollo_http_shared::method::normalize_method;
14pub(crate) use apollo_http_shared::protocol::{ProtocolVersionCell, version_str};
15
16use crate::error::HttpClientError;
17
18pub(crate) mod alpn;
19pub(crate) mod h1;
20pub(crate) mod h2;
21pub(crate) mod handshake;
22#[cfg(unix)]
23pub(crate) mod unix;
24
25#[derive(Clone, Debug, Hash, Eq, PartialEq)]
27pub(crate) struct Origin {
28 pub(crate) scheme: http::uri::Scheme,
29 pub(crate) authority: http::uri::Authority,
30}
31
32impl Origin {
33 pub(crate) fn address(&self) -> String {
37 if self.scheme.as_str() == "unix" {
38 #[cfg(unix)]
39 return crate::protocol::unix::authority_to_str(&self.authority)
40 .unwrap_or_else(|_| INVALID_SOCKET_PATH.to_owned());
41 #[cfg(not(unix))]
42 return INVALID_SOCKET_PATH.to_owned();
43 }
44 self.authority.host().to_owned()
45 }
46
47 pub(crate) fn port(&self) -> Option<i64> {
50 if self.scheme.as_str() == "unix" {
51 return None;
52 }
53 let port = self.authority.port_u16().map(i64::from).unwrap_or_else(|| {
54 if self.scheme == http::uri::Scheme::HTTPS {
55 443
56 } else {
57 80
58 }
59 });
60 Some(port)
61 }
62}
63
64pub(crate) const INVALID_SOCKET_PATH: &str = "<invalid socket path>";
68
69pub type HttpBody = BoxBody<Bytes, BoxError>;
89
90pub(crate) type SendFuture = Pin<
94 Box<dyn Future<Output = Result<http::Response<Incoming>, HttpClientError>> + Send + 'static>,
95>;
96
97pub(crate) type HostSender =
102 Arc<dyn Fn(http::Request<HttpBody>, ProtocolVersionCell) -> SendFuture + Send + Sync>;
103
104pub(super) fn spawn_eviction_task<C>(
112 cache: &Arc<Mutex<C>>,
113 interval: Duration,
114 mut evict: impl FnMut(&mut C, Instant) + Send + 'static,
115) -> tokio_util::sync::DropGuard
116where
117 C: Send + 'static,
118{
119 let cancel = CancellationToken::new();
120 tokio::spawn({
121 let weak = Arc::downgrade(cache);
122 let cancel = cancel.clone();
123 async move {
124 loop {
125 tokio::select! {
126 _ = cancel.cancelled() => break,
127 _ = tokio::time::sleep(interval) => {}
128 }
129 let Some(arc) = weak.upgrade() else { break };
130 evict(&mut arc.lock().unwrap(), Instant::now());
131 }
132 }
133 });
134 cancel.drop_guard()
135}
136
137pub(crate) async fn connect_with_timeout<S, IO>(
146 mut connector: S,
147 origin: Origin,
148 timeout: std::time::Duration,
149) -> Result<IO, HttpClientError>
150where
151 S: tower::Service<http::Uri, Response = IO>,
152 S::Error: Into<BoxError>,
153{
154 let uri = http::Uri::builder()
155 .scheme(origin.scheme)
156 .authority(origin.authority)
157 .path_and_query("/")
158 .build()
159 .map_err(|source| HttpClientError::InvalidUri { source })?;
160 tokio::time::timeout(timeout, connector.call(uri))
161 .await
162 .map_err(|_| HttpClientError::ConnectionTimeout)?
163 .map_err(|e| HttpClientError::Transport { source: e.into() })
164}
165
166pub(crate) fn into_http_client_error(e: BoxError) -> HttpClientError {
174 match e.downcast::<HttpClientError>() {
175 Ok(http_err) => *http_err,
176 Err(other) => HttpClientError::Transport { source: other },
177 }
178}
179
180pub(crate) fn retain_predicate(
187 idle_since: Option<Instant>,
188 idle_timeout: Duration,
189 now: Instant,
190 idle_count: &mut usize,
191 max_idle: usize,
192) -> bool {
193 match idle_since {
194 Some(t) => {
195 if now.duration_since(t) >= idle_timeout {
196 return false;
197 }
198 *idle_count += 1;
199 *idle_count <= max_idle
200 }
201 None => true,
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use std::time::Duration;
208
209 use super::{HttpClientError, into_http_client_error, retain_predicate};
210
211 #[test]
212 fn into_http_client_error_unwraps_boxed_proxy_tunnel() {
213 let original = HttpClientError::ProxyTunnel {
214 status: http::StatusCode::PROXY_AUTHENTICATION_REQUIRED,
215 };
216 let boxed: tower::BoxError = Box::new(original);
217
218 let err = into_http_client_error(boxed);
219 assert!(
220 matches!(
221 err,
222 HttpClientError::ProxyTunnel {
223 status: http::StatusCode::PROXY_AUTHENTICATION_REQUIRED,
224 }
225 ),
226 "expected ProxyTunnel, got {err}"
227 );
228 }
229
230 #[test]
231 fn into_http_client_error_unwraps_boxed_connection_timeout() {
232 let boxed: tower::BoxError = Box::new(HttpClientError::ConnectionTimeout);
233
234 let err = into_http_client_error(boxed);
235 assert!(
236 matches!(err, HttpClientError::ConnectionTimeout),
237 "expected ConnectionTimeout, got {err}"
238 );
239 }
240
241 #[test]
242 fn into_http_client_error_wraps_unstructured_box_error_as_transport() {
243 let boxed: tower::BoxError = "io error".into();
244
245 let err = into_http_client_error(boxed);
246 assert!(
247 matches!(err, HttpClientError::Transport { .. }),
248 "expected Transport, got {err}"
249 );
250 }
251
252 #[tokio::test(start_paused = true)]
255 async fn active_connection_always_retained() {
256 let now = tokio::time::Instant::now();
257 let mut count = 0;
258 assert!(retain_predicate(
259 None,
260 Duration::from_secs(1),
261 now,
262 &mut count,
263 1
264 ));
265 assert_eq!(count, 0);
266 }
267
268 #[tokio::test(start_paused = true)]
270 async fn idle_within_timeout_retained() {
271 let idle_since = tokio::time::Instant::now();
272 tokio::time::advance(Duration::from_secs(4)).await;
273 let now = tokio::time::Instant::now();
274 let mut count = 0;
275 assert!(retain_predicate(
276 Some(idle_since),
277 Duration::from_secs(5),
278 now,
279 &mut count,
280 1
281 ));
282 assert_eq!(count, 1);
283 }
284
285 #[tokio::test(start_paused = true)]
287 async fn idle_past_timeout_evicted() {
288 let idle_since = tokio::time::Instant::now();
289 tokio::time::advance(Duration::from_secs(6)).await;
290 let now = tokio::time::Instant::now();
291 let mut count = 0;
292 assert!(!retain_predicate(
293 Some(idle_since),
294 Duration::from_secs(5),
295 now,
296 &mut count,
297 1
298 ));
299 }
300
301 #[tokio::test(start_paused = true)]
304 async fn idle_count_limit_evicts_excess() {
305 let idle_since = tokio::time::Instant::now();
306 let now = tokio::time::Instant::now();
307 let mut count = 0;
308 assert!(retain_predicate(
309 Some(idle_since),
310 Duration::from_secs(60),
311 now,
312 &mut count,
313 2
314 ));
315 assert!(retain_predicate(
316 Some(idle_since),
317 Duration::from_secs(60),
318 now,
319 &mut count,
320 2
321 ));
322 assert!(!retain_predicate(
323 Some(idle_since),
324 Duration::from_secs(60),
325 now,
326 &mut count,
327 2
328 ));
329 }
330}