Skip to main content

trillium_client/
client.rs

1use crate::{Conn, IntoUrl, Pool, USER_AGENT, h3::H3ClientState};
2use std::{fmt::Debug, sync::Arc, time::Duration};
3use trillium_http::{
4    HeaderName, HeaderValues, Headers, HttpContext, KnownHeaderName, Method, ReceivedBodyState,
5    TypeSet, Version::Http1_1,
6};
7use trillium_server_common::{
8    ArcedConnector, ArcedQuicClientConfig, Connector, QuicClientConfig, Transport,
9    url::{Origin, Url},
10};
11
12/// A HTTP Client supporting HTTP/1.x and, when configured with a quic implementation, HTTP/3. See
13/// [`Client::new`] and [`Client::new_with_quic`] for construction information.
14#[derive(Clone, Debug, fieldwork::Fieldwork)]
15pub struct Client {
16    config: ArcedConnector,
17    h3: Option<H3ClientState>,
18    pool: Option<Pool<Origin, Box<dyn Transport>>>,
19
20    /// url base for this client
21    #[field(get)]
22    base: Option<Arc<Url>>,
23
24    /// default request headers
25    #[field(get)]
26    default_headers: Arc<Headers>,
27
28    /// optional timeout
29    #[field(get, set, with, copy, option_set_some)]
30    timeout: Option<Duration>,
31
32    /// configuration
33    #[field(get, get_mut, set, with, into)]
34    context: Arc<HttpContext>,
35}
36
37macro_rules! method {
38    ($fn_name:ident, $method:ident) => {
39        method!(
40            $fn_name,
41            $method,
42            concat!(
43                // yep, macro-generated doctests
44                "Builds a new client conn with the ",
45                stringify!($fn_name),
46                " http method and the provided url.
47
48```
49use trillium_client::{Client, Method};
50use trillium_testing::client_config;
51
52let client = Client::new(client_config());
53let conn = client.",
54                stringify!($fn_name),
55                "(\"http://localhost:8080/some/route\"); //<-
56
57assert_eq!(conn.method(), Method::",
58                stringify!($method),
59                ");
60assert_eq!(conn.url().to_string(), \"http://localhost:8080/some/route\");
61```
62"
63            )
64        );
65    };
66
67    ($fn_name:ident, $method:ident, $doc_comment:expr_2021) => {
68        #[doc = $doc_comment]
69        pub fn $fn_name(&self, url: impl IntoUrl) -> Conn {
70            self.build_conn(Method::$method, url)
71        }
72    };
73}
74
75pub(crate) fn default_request_headers() -> Headers {
76    Headers::new()
77        .with_inserted_header(KnownHeaderName::UserAgent, USER_AGENT)
78        .with_inserted_header(KnownHeaderName::Accept, "*/*")
79}
80
81impl Client {
82    method!(get, Get);
83
84    method!(post, Post);
85
86    method!(put, Put);
87
88    method!(delete, Delete);
89
90    method!(patch, Patch);
91
92    /// builds a new client from this `Connector`
93    pub fn new(connector: impl Connector) -> Self {
94        Self {
95            config: ArcedConnector::new(connector),
96            h3: None,
97            pool: Some(Pool::default()),
98            base: None,
99            default_headers: Arc::new(default_request_headers()),
100            timeout: None,
101            context: Default::default(),
102        }
103    }
104
105    /// Build a new client with both a TCP connector and a QUIC connector for HTTP/3 support.
106    ///
107    /// The connector's runtime and UDP socket type are bound to the QUIC connector here,
108    /// before type erasure, so that `trillium-quinn` and the runtime adapter remain
109    /// independent crates that neither depends on the other.
110    ///
111    /// When H3 is configured, the client will track `Alt-Svc` headers in responses and
112    /// automatically use HTTP/3 for subsequent requests to origins that advertise it.
113    /// Requests to origins without a cached alt-svc entry continue to use HTTP/1.1.
114    pub fn new_with_quic<C: Connector, Q: QuicClientConfig<C>>(connector: C, quic: Q) -> Self {
115        // Bind the runtime into the QUIC client config before consuming `connector`.
116        let arced_quic = ArcedQuicClientConfig::new(&connector, quic);
117        Self {
118            config: ArcedConnector::new(connector),
119            h3: Some(H3ClientState::new(arced_quic)),
120            pool: Some(Pool::default()),
121            base: None,
122            default_headers: Arc::new(default_request_headers()),
123            timeout: None,
124            context: Default::default(),
125        }
126    }
127
128    /// chainable method to remove a header from default request headers
129    pub fn without_default_header(mut self, name: impl Into<HeaderName<'static>>) -> Self {
130        self.default_headers_mut().remove(name);
131        self
132    }
133
134    /// chainable method to insert a new default request header, replacing any existing value
135    pub fn with_default_header(
136        mut self,
137        name: impl Into<HeaderName<'static>>,
138        value: impl Into<HeaderValues>,
139    ) -> Self {
140        self.default_headers_mut().insert(name, value);
141        self
142    }
143
144    /// borrow the default headers mutably
145    ///
146    /// calling this will copy-on-write if the default headers are shared with another client clone
147    pub fn default_headers_mut(&mut self) -> &mut Headers {
148        Arc::make_mut(&mut self.default_headers)
149    }
150
151    /// chainable constructor to disable http/1.1 connection reuse.
152    ///
153    /// ```
154    /// use trillium_client::Client;
155    /// use trillium_smol::ClientConfig;
156    ///
157    /// let client = Client::new(ClientConfig::default()).without_keepalive();
158    /// ```
159    pub fn without_keepalive(mut self) -> Self {
160        self.pool = None;
161        self
162    }
163
164    /// builds a new conn.
165    ///
166    /// if the client has pooling enabled and there is
167    /// an available connection to the dns-resolved socket (ip and port),
168    /// the new conn will reuse that when it is sent.
169    ///
170    /// ```
171    /// use trillium_client::{Client, Method};
172    /// use trillium_smol::ClientConfig;
173    /// let client = Client::new(ClientConfig::default());
174    ///
175    /// let conn = client.build_conn("get", "http://trillium.rs"); //<-
176    ///
177    /// assert_eq!(conn.method(), Method::Get);
178    /// assert_eq!(conn.url().host_str().unwrap(), "trillium.rs");
179    /// ```
180    pub fn build_conn<M>(&self, method: M, url: impl IntoUrl) -> Conn
181    where
182        M: TryInto<Method>,
183        <M as TryInto<Method>>::Error: Debug,
184    {
185        let method = method.try_into().unwrap();
186        let (url, request_target) = if let Some(base) = &self.base
187            && let Some(request_target) = url.request_target(method)
188        {
189            ((**base).clone(), Some(request_target))
190        } else {
191            (self.build_url(url).unwrap(), None)
192        };
193
194        Conn {
195            url,
196            method,
197            request_headers: Headers::clone(&self.default_headers),
198            response_headers: Headers::new(),
199            transport: None,
200            status: None,
201            request_body: None,
202            pool: self.pool.clone(),
203            h3: self.h3.clone(),
204            buffer: Vec::with_capacity(128).into(),
205            response_body_state: ReceivedBodyState::Start,
206            config: self.config.clone(),
207            headers_finalized: false,
208            timeout: self.timeout,
209            http_version: Http1_1,
210            max_head_length: 8 * 1024,
211            state: TypeSet::new(),
212            context: self.context.clone(),
213            authority: None,
214            scheme: None,
215            path: None,
216            request_target,
217            request_trailers: None,
218            response_trailers: None,
219        }
220    }
221
222    /// borrow the connector for this client
223    pub fn connector(&self) -> &ArcedConnector {
224        &self.config
225    }
226
227    /// The pool implementation currently accumulates a small memory
228    /// footprint for each new host. If your application is reusing a pool
229    /// against a large number of unique hosts, call this method
230    /// intermittently.
231    pub fn clean_up_pool(&self) {
232        if let Some(pool) = &self.pool {
233            pool.cleanup();
234        }
235    }
236
237    /// chainable method to set the base for this client
238    pub fn with_base(mut self, base: impl IntoUrl) -> Self {
239        self.set_base(base).unwrap();
240        self
241    }
242
243    /// attempt to build a url from this IntoUrl and the [`Client::base`], if set
244    pub fn build_url(&self, url: impl IntoUrl) -> crate::Result<Url> {
245        url.into_url(self.base())
246    }
247
248    /// set the base for this client
249    pub fn set_base(&mut self, base: impl IntoUrl) -> crate::Result<()> {
250        let mut base = base.into_url(None)?;
251
252        if !base.path().ends_with('/') {
253            log::warn!("appending a trailing / to {base}");
254            base.set_path(&format!("{}/", base.path()));
255        }
256
257        self.base = Some(Arc::new(base));
258        Ok(())
259    }
260
261    /// Mutate the url base for this client.
262    ///
263    /// This has "clone-on-write" semantics if there are other clones of this client. If there are
264    /// other clones of this client, they will not be updated.
265    pub fn base_mut(&mut self) -> Option<&mut Url> {
266        let base = self.base.as_mut()?;
267        Some(Arc::make_mut(base))
268    }
269}
270
271impl<T: Connector> From<T> for Client {
272    fn from(connector: T) -> Self {
273        Self::new(connector)
274    }
275}