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, error) = if let Some(base) = &self.base
311 && let Some(request_target) = url.request_target(method)
312 {
313 ((**base).clone(), Some(request_target), None)
314 } else {
315 match self.build_url(url) {
316 Ok(url) => (url, None, None),
317 Err(error) => (
321 Url::parse("http://invalid.invalid/").expect("literal is a valid url"),
322 None,
323 Some(error),
324 ),
325 }
326 };
327
328 Conn {
329 url,
330 method,
331 request_headers: Headers::clone(&self.default_headers),
332 response_headers: Headers::new(),
333 transport: None,
334 status: None,
335 request_body: None,
336 protocol_session: ProtocolSession::Http1,
337 #[cfg(feature = "webtransport")]
338 wt_pool_entry: None,
339 buffer: Vec::with_capacity(128).into(),
340 response_body_state: ReceivedBodyState::End,
341 headers_finalized: false,
342 halted: false,
343 error,
344 body_override: None,
345 timeout: self.timeout,
346 http_version: None,
347 max_head_length: 8 * 1024,
348 state: TypeSet::new(),
349 context: self.context.clone(),
350 authority: None,
351 scheme: None,
352 path: None,
353 request_target,
354 protocol: None,
355 request_trailers: None,
356 response_trailers: None,
357 client: self.clone(),
358 followup: None,
359 upgrade: false,
360 }
361 }
362
363 pub fn connector(&self) -> &ArcedConnector {
365 &self.config
366 }
367
368 pub fn clean_up_pool(&self) {
372 if let Some(pool) = &self.pool {
373 pool.cleanup();
374 }
375 if let Some(h2_pool) = &self.h2_pool {
376 h2_pool.cleanup();
377 }
378 }
379
380 pub fn with_base(mut self, base: impl IntoUrl) -> Self {
382 self.set_base(base).unwrap();
383 self
384 }
385
386 pub fn build_url(&self, url: impl IntoUrl) -> crate::Result<Url> {
388 url.into_url(self.base())
389 }
390
391 pub fn set_base(&mut self, base: impl IntoUrl) -> crate::Result<()> {
393 let mut base = base.into_url(None)?;
394
395 if !base.path().ends_with('/') {
396 log::warn!("appending a trailing / to {base}");
397 base.set_path(&format!("{}/", base.path()));
398 }
399
400 self.base = Some(Arc::new(base));
401 Ok(())
402 }
403
404 pub fn base_mut(&mut self) -> Option<&mut Url> {
409 let base = self.base.as_mut()?;
410 Some(Arc::make_mut(base))
411 }
412}
413
414impl<T: Connector> From<T> for Client {
415 fn from(connector: T) -> Self {
416 Self::new(connector)
417 }
418}