1use crate::{
2 ClientHandler, Conn, IntoUrl, Pool, USER_AGENT, client_handler::ArcedClientHandler,
3 conn::H2Pooled, h3::H3ClientState,
4};
5use std::{any::Any, fmt::Debug, sync::Arc, time::Duration};
6use trillium_http::{
7 HeaderName, HeaderValues, Headers, HttpContext, KnownHeaderName, Method, ProtocolSession,
8 ReceivedBodyState, TypeSet,
9};
10use trillium_server_common::{
11 ArcedConnector, ArcedQuicClientConfig, Connector, QuicClientConfig, Transport,
12 url::{Origin, Url},
13};
14
15const DEFAULT_H2_IDLE_TIMEOUT: Duration = Duration::from_secs(300);
19
20const DEFAULT_H2_IDLE_PING_THRESHOLD: Duration = Duration::from_secs(10);
23
24const DEFAULT_H2_IDLE_PING_TIMEOUT: Duration = Duration::from_secs(20);
27
28#[derive(Clone, Debug, fieldwork::Fieldwork)]
32pub struct Client {
33 config: ArcedConnector,
34
35 #[field(vis = "pub(crate)", get)]
36 h3: Option<H3ClientState>,
37
38 #[field(vis = "pub(crate)", get)]
39 pool: Option<Pool<Origin, Box<dyn Transport>>>,
40
41 #[field(vis = "pub(crate)", get)]
42 h2_pool: Option<Pool<Origin, H2Pooled>>,
43
44 #[field(get, set, with, without, copy)]
48 h2_idle_timeout: Option<Duration>,
49
50 #[field(get, set, with, copy, without)]
55 h2_idle_ping_threshold: Option<Duration>,
56
57 #[field(get, set, with, copy)]
64 h2_idle_ping_timeout: Duration,
65
66 #[field(get)]
68 base: Option<Arc<Url>>,
69
70 #[field(get)]
72 default_headers: Arc<Headers>,
73
74 #[field(get, set, with, copy, without, option_set_some)]
76 timeout: Option<Duration>,
77
78 #[field(get, get_mut, set, with, into)]
80 context: Arc<HttpContext>,
81
82 #[field(vis = "pub(crate)", get = arc_handler)]
86 handler: ArcedClientHandler,
87
88 #[cfg(feature = "hickory")]
91 pub(crate) resolver: Option<crate::dns::Resolver>,
92}
93
94macro_rules! method {
95 ($fn_name:ident, $method:ident) => {
96 method!(
97 $fn_name,
98 $method,
99 concat!(
100 "Builds a new client conn with the ",
102 stringify!($fn_name),
103 " http method and the provided url.
104
105```
106use trillium_client::{Client, Method};
107use trillium_testing::client_config;
108
109let client = Client::new(client_config());
110let conn = client.",
111 stringify!($fn_name),
112 "(\"http://localhost:8080/some/route\"); //<-
113
114assert_eq!(conn.method(), Method::",
115 stringify!($method),
116 ");
117assert_eq!(conn.url().to_string(), \"http://localhost:8080/some/route\");
118```
119"
120 )
121 );
122 };
123
124 ($fn_name:ident, $method:ident, $doc_comment:expr_2021) => {
125 #[doc = $doc_comment]
126 pub fn $fn_name(&self, url: impl IntoUrl) -> Conn {
127 self.build_conn(Method::$method, url)
128 }
129 };
130}
131
132pub(crate) fn default_request_headers() -> Headers {
133 Headers::new()
134 .with_inserted_header(KnownHeaderName::UserAgent, USER_AGENT)
135 .with_inserted_header(KnownHeaderName::Accept, "*/*")
136}
137
138impl Client {
139 method!(get, Get);
140
141 method!(post, Post);
142
143 method!(put, Put);
144
145 method!(delete, Delete);
146
147 method!(patch, Patch);
148
149 pub fn new(connector: impl Connector) -> Self {
151 Self {
152 config: ArcedConnector::new(connector),
153 h3: None,
154 pool: Some(Pool::default()),
155 h2_pool: Some(Pool::default()),
156 h2_idle_timeout: Some(DEFAULT_H2_IDLE_TIMEOUT),
157 h2_idle_ping_threshold: Some(DEFAULT_H2_IDLE_PING_THRESHOLD),
158 h2_idle_ping_timeout: DEFAULT_H2_IDLE_PING_TIMEOUT,
159 base: None,
160 default_headers: Arc::new(default_request_headers()),
161 timeout: None,
162 context: Default::default(),
163 handler: ArcedClientHandler::new(()),
164 #[cfg(feature = "hickory")]
165 resolver: None,
166 }
167 }
168
169 pub fn new_with_quic<C: Connector, Q: QuicClientConfig<C>>(connector: C, quic: Q) -> Self {
179 let arced_quic = ArcedQuicClientConfig::new(&connector, quic);
181
182 #[cfg_attr(not(feature = "webtransport"), allow(unused_mut))]
183 let mut context = HttpContext::default();
184 #[cfg(feature = "webtransport")]
185 {
186 context
191 .config_mut()
192 .set_h3_datagrams_enabled(true)
193 .set_webtransport_enabled(true)
194 .set_extended_connect_enabled(true);
195 }
196
197 Self {
198 config: ArcedConnector::new(connector),
199 h3: Some(H3ClientState::new(arced_quic)),
200 pool: Some(Pool::default()),
201 h2_pool: Some(Pool::default()),
202 h2_idle_timeout: Some(DEFAULT_H2_IDLE_TIMEOUT),
203 h2_idle_ping_threshold: Some(DEFAULT_H2_IDLE_PING_THRESHOLD),
204 h2_idle_ping_timeout: DEFAULT_H2_IDLE_PING_TIMEOUT,
205 base: None,
206 default_headers: Arc::new(default_request_headers()),
207 timeout: None,
208 context: Arc::new(context),
209 handler: ArcedClientHandler::new(()),
210 #[cfg(feature = "hickory")]
211 resolver: None,
212 }
213 }
214
215 #[must_use]
224 pub fn with_handler<H: ClientHandler>(mut self, handler: H) -> Self {
225 self.set_handler(handler);
226 self
227 }
228
229 pub fn set_handler<H: ClientHandler>(&mut self, handler: H) -> &mut Self {
232 self.handler = ArcedClientHandler::new(handler);
233 self
234 }
235
236 pub fn handler(&self) -> &impl ClientHandler {
240 &self.handler
241 }
242
243 pub fn downcast_handler<T: Any + 'static>(&self) -> Option<&T> {
249 self.handler.downcast_ref()
250 }
251
252 pub fn without_default_header(mut self, name: impl Into<HeaderName<'static>>) -> Self {
254 self.default_headers_mut().remove(name);
255 self
256 }
257
258 pub fn with_default_header(
260 mut self,
261 name: impl Into<HeaderName<'static>>,
262 value: impl Into<HeaderValues>,
263 ) -> Self {
264 self.default_headers_mut().insert(name, value);
265 self
266 }
267
268 pub fn default_headers_mut(&mut self) -> &mut Headers {
272 Arc::make_mut(&mut self.default_headers)
273 }
274
275 pub fn without_keepalive(mut self) -> Self {
284 self.pool = None;
285 self.h2_pool = None;
286 self
287 }
288
289 pub fn build_conn<M>(&self, method: M, url: impl IntoUrl) -> Conn
305 where
306 M: TryInto<Method>,
307 <M as TryInto<Method>>::Error: Debug,
308 {
309 let method = method.try_into().unwrap();
310 let (url, request_target) = if let Some(base) = &self.base
311 && let Some(request_target) = url.request_target(method)
312 {
313 ((**base).clone(), Some(request_target))
314 } else {
315 (self.build_url(url).unwrap(), None)
316 };
317
318 Conn {
319 url,
320 method,
321 request_headers: Headers::clone(&self.default_headers),
322 response_headers: Headers::new(),
323 transport: None,
324 status: None,
325 request_body: None,
326 protocol_session: ProtocolSession::Http1,
327 #[cfg(feature = "webtransport")]
328 wt_pool_entry: None,
329 buffer: Vec::with_capacity(128).into(),
330 response_body_state: ReceivedBodyState::End,
331 headers_finalized: false,
332 halted: false,
333 error: None,
334 body_override: None,
335 timeout: self.timeout,
336 http_version: None,
337 max_head_length: 8 * 1024,
338 state: TypeSet::new(),
339 context: self.context.clone(),
340 authority: None,
341 scheme: None,
342 path: None,
343 request_target,
344 protocol: None,
345 request_trailers: None,
346 response_trailers: None,
347 client: self.clone(),
348 followup: None,
349 upgrade: false,
350 }
351 }
352
353 pub fn connector(&self) -> &ArcedConnector {
355 &self.config
356 }
357
358 pub fn clean_up_pool(&self) {
362 if let Some(pool) = &self.pool {
363 pool.cleanup();
364 }
365 if let Some(h2_pool) = &self.h2_pool {
366 h2_pool.cleanup();
367 }
368 }
369
370 pub fn with_base(mut self, base: impl IntoUrl) -> Self {
372 self.set_base(base).unwrap();
373 self
374 }
375
376 pub fn build_url(&self, url: impl IntoUrl) -> crate::Result<Url> {
378 url.into_url(self.base())
379 }
380
381 pub fn set_base(&mut self, base: impl IntoUrl) -> crate::Result<()> {
383 let mut base = base.into_url(None)?;
384
385 if !base.path().ends_with('/') {
386 log::warn!("appending a trailing / to {base}");
387 base.set_path(&format!("{}/", base.path()));
388 }
389
390 self.base = Some(Arc::new(base));
391 Ok(())
392 }
393
394 pub fn base_mut(&mut self) -> Option<&mut Url> {
399 let base = self.base.as_mut()?;
400 Some(Arc::make_mut(base))
401 }
402}
403
404impl<T: Connector> From<T> for Client {
405 fn from(connector: T) -> Self {
406 Self::new(connector)
407 }
408}