Skip to main content

trillium_client/conn/
shared.rs

1use super::{Body, Conn, Transport, TypeSet};
2use crate::{ClientHandler, ConnExt, Error, Result, Version};
3use smallvec::SmallVec;
4#[cfg(feature = "hickory")]
5use std::net::IpAddr;
6use std::{
7    borrow::Cow,
8    fmt::{self, Debug, Formatter},
9    future::{Future, IntoFuture},
10    mem,
11    net::SocketAddr,
12    pin::Pin,
13};
14use trillium_http::{ProtocolSession, Upgrade};
15use trillium_server_common::Destination;
16
17/// A wrapper error for [`trillium_http::Error`] or, depending on json serializer feature, either
18/// `sonic_rs::Error` or `serde_json::Error`. Only available when either the `sonic-rs` or
19/// `serde_json` cargo features are enabled.
20#[cfg(any(feature = "serde_json", feature = "sonic-rs"))]
21#[derive(thiserror::Error, Debug)]
22pub enum ClientSerdeError {
23    /// A [`trillium_http::Error`]
24    #[error(transparent)]
25    HttpError(#[from] Error),
26
27    #[cfg(feature = "sonic-rs")]
28    /// A [`sonic_rs::Error`]
29    #[error(transparent)]
30    JsonError(#[from] sonic_rs::Error),
31
32    #[cfg(feature = "serde_json")]
33    /// A [`serde_json::Error`]
34    #[error(transparent)]
35    JsonError(#[from] serde_json::Error),
36}
37
38impl Conn {
39    pub(crate) async fn exec(&mut self) -> Result<()> {
40        // A build-time error (e.g. a malformed url from `build_conn`) is fatal and
41        // unrecoverable: there is nothing to dial, so short-circuit before running
42        // handlers or touching the network.
43        if let Some(error) = self.error.take() {
44            return Err(error);
45        }
46
47        // Arc-clone to dodge conflict with the `&mut self` we pass to `run`.
48        let handler = self.client.arc_handler().clone();
49        handler.run(self).await?;
50
51        if !self.halted {
52            // Stash, don't return: `after_response` runs unconditionally so recovery handlers
53            // (stale-if-error, retry-with-fallback) get a chance to clear it.
54            if let Err(e) = self.exec_network().await {
55                self.error = Some(e);
56            }
57        } else {
58            log::trace!("conn is halted, skipping network round-trip");
59            // The request body is never sent when halted, so drop it here — matching the
60            // send paths, which all `.take()` it. A streaming body backed by an external
61            // producer would otherwise keep that producer parked, waiting for a read that
62            // never comes.
63            self.request_body = None;
64        }
65
66        // Reverse order, regardless of halt/error — mirrors server-side `before_send`.
67        handler.after_response(self).await?;
68
69        if let Some(e) = self.error.take() {
70            Err(e)
71        } else {
72            Ok(())
73        }
74    }
75
76    async fn exec_network(&mut self) -> Result<()> {
77        if self.http_version == Some(Version::Http0_9) {
78            return Err(Error::UnsupportedVersion(Version::Http0_9));
79        }
80
81        match self.exec_network_dispatch().await {
82            // The h2/h3 connection stays pooled for ordinary requests; this request re-runs as
83            // an HTTP/1.1 upgrade on a connection pinned to h1. The extended-CONNECT gates run
84            // before method, version, or pseudo-headers are committed, so the only state
85            // carried over is h1 handshake headers from an ALPN promotion, which the h1 path
86            // re-renders idempotently.
87            Err(Error::ExtendedConnectUnsupported)
88                if !self.strict_http_version && self.protocol.is_some() =>
89            {
90                log::debug!(
91                    "peer does not support extended CONNECT; retrying as an HTTP/1.1 upgrade"
92                );
93                self.http_version = Some(Version::Http1_1);
94                self.headers_finalized = false;
95                self.exec_h1_or_promote_h2().await
96            }
97            other => other,
98        }
99    }
100
101    async fn exec_network_dispatch(&mut self) -> Result<()> {
102        // Phase 1 — reuse a live pooled connection, best protocol first. No DNS, no new connect.
103        // A pooled h2 connection is reused in preference to establishing a new h3 connection: we
104        // do not proactively migrate h2→h3, since a general-purpose client can't assume the
105        // request locality that makes eager migration pay off (see the migration-policy backlog
106        // item). A pooled h1 connection, by contrast, does not block establishing h3 below.
107        if self.try_reuse_h3_pool().await? {
108            return Ok(());
109        }
110        if self.try_exec_h2_pooled().await? {
111            return Ok(());
112        }
113
114        // Phase 2/3 — establish a new connection, preferring h3 when the origin is known to speak
115        // it (pinned, Alt-Svc, or SVCB). This runs before the h1 path, so h1→h3 is immediate.
116        if self.try_establish_h3().await? {
117            return Ok(());
118        }
119
120        // Prior-knowledge h2: caller asserted h2, skip h1/ALPN. Useful for TLS connectors
121        // that don't expose `negotiated_alpn` (e.g. native-tls). No fallback — a non-h2
122        // server here surfaces as a plain IO error.
123        if self.http_version == Some(Version::Http2) {
124            return self.exec_h2_prior_knowledge().await;
125        }
126
127        self.exec_h1_or_promote_h2().await
128    }
129
130    pub(crate) fn body_len(&self) -> Option<u64> {
131        if let Some(ref body) = self.request_body {
132            body.len()
133        } else {
134            Some(0)
135        }
136    }
137
138    pub(crate) fn finalize_headers(&mut self) -> Result<()> {
139        match self.http_version() {
140            Version::Http1_0 | Version::Http1_1 => self.finalize_headers_h1(),
141            Version::Http2 => self.finalize_headers_h2(),
142            Version::Http3 if self.client.h3().is_some() => self.finalize_headers_h3(),
143            other => Err(Error::UnsupportedVersion(other)),
144        }
145    }
146
147    /// The [`Destination`] for connecting to this conn's origin over h1/h2: scheme, host, and port
148    /// from the URL, plus any DoH-resolved addresses. A bare-IP origin keeps the address
149    /// [`from_url`](Destination::from_url) derived and is never resolved.
150    ///
151    /// An explicit version pin constrains the connection's ALPN so the pin is honored over TLS: an
152    /// h1 pin advertises only `http/1.1` (a server that would otherwise negotiate `h2` falls back
153    /// to h1), an h2 pin advertises only `h2`. Without a pin the connector's configured default
154    /// ALPN is left in place, so auto-discovery can promote to h2 via ALPN.
155    pub(crate) async fn origin_destination(&self) -> Result<Destination> {
156        let mut destination = Destination::from_url(&self.url)?;
157        let addrs = self.origin_socket_addrs().await?;
158        if !addrs.is_empty() {
159            destination.set_addrs(addrs);
160        }
161        match self.http_version {
162            Some(Version::Http1_0 | Version::Http1_1) => {
163                destination.set_alpn([Cow::Borrowed(b"http/1.1".as_slice())]);
164            }
165            Some(Version::Http2) => {
166                destination.set_alpn([Cow::Borrowed(b"h2".as_slice())]);
167            }
168            _ => {}
169        }
170        Ok(destination)
171    }
172
173    /// Pre-resolved socket addresses for this conn's origin host:port, for the protocols that
174    /// always connect to the origin (h1/h2). Empty when DoH is not configured or the host is an IP
175    /// literal, so the connector falls back to its own (trivial, for an IP) resolution.
176    pub(crate) async fn origin_socket_addrs(&self) -> Result<SmallVec<[SocketAddr; 4]>> {
177        let Some(host) = self.url.host_str() else {
178            return Ok(SmallVec::new());
179        };
180        let port = self.url.port_or_known_default().unwrap_or(443);
181        self.resolve_socket_addrs(host, port).await
182    }
183}
184
185#[cfg(feature = "hickory")]
186impl Conn {
187    /// Resolve `host:port` through the configured DoH resolver, or `None` when DoH is not
188    /// configured (so the caller falls back to the connector's own resolution).
189    ///
190    /// The single place this conn touches DNS. The resolver reads and populates a shared, TTL'd
191    /// cache as a side effect, so repeated calls for the same host — across protocols, and across
192    /// the SVCB decision and the eventual connect — issue at most one set of queries.
193    ///
194    /// Fail-closed: once DoH is configured, a lookup the resolver can't answer fails the request
195    /// rather than falling back to the (possibly plaintext) system resolver.
196    ///
197    /// An IP-literal host is returned as `None` without touching the resolver — there is nothing to
198    /// look up, and no SVCB/HTTPS records exist for a bare address.
199    pub(crate) async fn resolve(
200        &self,
201        host: &str,
202        port: u16,
203    ) -> Result<Option<crate::dns::Resolved>> {
204        if host.parse::<IpAddr>().is_ok() {
205            return Ok(None);
206        }
207        match &self.client.resolver {
208            Some(resolver) => Ok(Some(
209                resolver
210                    .resolve(&self.client, host, port, self.timeout)
211                    .await?,
212            )),
213            None => Ok(None),
214        }
215    }
216
217    pub(crate) async fn resolve_socket_addrs(
218        &self,
219        host: &str,
220        port: u16,
221    ) -> Result<SmallVec<[SocketAddr; 4]>> {
222        Ok(self
223            .resolve(host, port)
224            .await?
225            .map(|resolved| resolved.socket_addrs(port))
226            .unwrap_or_default())
227    }
228}
229
230#[cfg(not(feature = "hickory"))]
231impl Conn {
232    pub(crate) async fn resolve_socket_addrs(
233        &self,
234        _host: &str,
235        _port: u16,
236    ) -> Result<SmallVec<[SocketAddr; 4]>> {
237        Ok(SmallVec::new())
238    }
239}
240
241impl Drop for Conn {
242    fn drop(&mut self) {
243        log::trace!("dropping client conn");
244        drop(self.take_response_body());
245    }
246}
247
248impl From<Conn> for Body {
249    fn from(mut conn: Conn) -> Body {
250        // body_override (e.g. cache hit, set via `set_response_body`) bypasses the transport;
251        // transport pooling is left to `Drop`.
252        if let Some(body) = conn.body_override.take() {
253            return body;
254        }
255
256        match conn.take_received_body(true) {
257            Some(rb) => rb.into(),
258            None => Body::default(),
259        }
260    }
261}
262
263impl From<Conn> for Upgrade<Box<dyn Transport>> {
264    /// Convert a client conn into a [`trillium_http::Upgrade`] after response headers
265    /// arrive, handing off the open transport for direct `AsyncRead` / `AsyncWrite`
266    /// exchange with per-protocol framing applied.
267    ///
268    /// # Panics
269    ///
270    /// Panics if the conn has no live transport (request not yet sent, or transport
271    /// already taken).
272    fn from(mut conn: Conn) -> Self {
273        // `Conn: Drop` rules out destructuring — pull each field with `mem::take` /
274        // `mem::replace`. New fields on `Conn` won't show up here automatically.
275        let path = conn.path.take().unwrap_or_else(|| match conn.url.query() {
276            Some(q) => Cow::Owned(format!("{}?{q}", conn.url.path())),
277            None => Cow::Owned(conn.url.path().to_owned()),
278        });
279        let secure = conn.url.scheme() == "https";
280
281        Upgrade::from_parts(
282            mem::take(&mut conn.response_headers),
283            mem::take(&mut conn.request_headers),
284            path,
285            conn.method,
286            conn.transport
287                .take()
288                .expect("client conn has no transport — request not yet sent"),
289            mem::take(&mut conn.buffer),
290            mem::take(&mut conn.state),
291            conn.context.clone(),
292            None,
293            conn.authority.take(),
294            conn.scheme.take(),
295            mem::replace(&mut conn.protocol_session, ProtocolSession::Http1),
296            conn.protocol.take(),
297            conn.http_version(),
298            conn.status,
299            secure,
300            // Client-side inbound = response body.
301            mem::take(&mut conn.response_body_state),
302            // Carry through any pre-upgrade-decoded trailers so a downstream reader
303            // can observe them.
304            conn.response_trailers.take(),
305        )
306    }
307}
308
309impl IntoFuture for Conn {
310    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'static>>;
311    type Output = Result<Conn>;
312
313    fn into_future(mut self) -> Self::IntoFuture {
314        Box::pin(async move { (&mut self).await.map(|()| self) })
315    }
316}
317
318impl<'conn> IntoFuture for &'conn mut Conn {
319    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'conn>>;
320    type Output = Result<()>;
321
322    fn into_future(self) -> Self::IntoFuture {
323        Box::pin(async move {
324            // Re-issuing handlers (FollowRedirects, retry, auth-refresh) queue a follow-up
325            // via `set_followup` in `after_response`; we recycle, swap, re-exec.
326            loop {
327                let result = if let Some(duration) = self.timeout {
328                    self.client
329                        .connector()
330                        .runtime()
331                        .timeout(duration, self.exec())
332                        .await
333                        .unwrap_or(Err(Error::TimedOut("Conn", duration)))
334                } else {
335                    self.exec().await
336                };
337
338                // `halted` is handler-internal; don't leak it out to the caller.
339                self.halted = false;
340
341                if let Err(e) = result {
342                    // Unrecovered error wins over any queued follow-up. Recovery handlers
343                    // that want the follow-up to run must `take_error()` in `after_response`.
344                    self.followup = None;
345                    return Err(e);
346                }
347
348                let Some(next) = self.take_followup() else {
349                    break;
350                };
351
352                if let Some(body) = self.take_response_body() {
353                    body.recycle().await;
354                }
355
356                let _displaced = mem::replace(self, next);
357            }
358            Ok(())
359        })
360    }
361}
362
363impl Debug for Conn {
364    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
365        f.debug_struct("Conn")
366            .field("authority", &self.authority)
367            .field("buffer", &String::from_utf8_lossy(&self.buffer))
368            .field("client", &self.client)
369            .field("protocol_session", &self.protocol_session)
370            .field("http_version", &self.http_version)
371            .field("method", &self.method)
372            .field("path", &self.path)
373            .field("request_body", &self.request_body)
374            .field("request_headers", &self.request_headers)
375            .field("request_target", &self.request_target)
376            .field("request_trailers", &self.request_trailers)
377            .field("response_body_state", &self.response_body_state)
378            .field("response_headers", &self.response_headers)
379            .field("response_trailers", &self.response_trailers)
380            .field("scheme", &self.scheme)
381            .field("state", &self.state)
382            .field("status", &self.status)
383            .field("url", &self.url)
384            .finish()
385    }
386}
387
388impl AsRef<TypeSet> for Conn {
389    fn as_ref(&self) -> &TypeSet {
390        &self.state
391    }
392}
393
394impl AsMut<TypeSet> for Conn {
395    fn as_mut(&mut self) -> &mut TypeSet {
396        &mut self.state
397    }
398}